Skip to content

Commit 97b9c17

Browse files
committed
2 parents 81113f2 + c8af6d6 commit 97b9c17

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+550
-388
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.zulipmobile
2+
3+
import android.util.Base64
4+
import com.facebook.react.bridge.Promise
5+
import com.facebook.react.bridge.ReactApplicationContext
6+
import com.facebook.react.bridge.ReactContextBaseJavaModule
7+
import com.facebook.react.bridge.ReactMethod
8+
import java.io.ByteArrayOutputStream
9+
import java.io.IOException
10+
import java.io.UnsupportedEncodingException
11+
import java.util.zip.DataFormatException
12+
import java.util.zip.Deflater
13+
import java.util.zip.Inflater
14+
15+
// TODO: Write unit tests; see
16+
// https://github.com/zulip/zulip-mobile/blob/main/docs/howto/testing.md#unit-tests-android.
17+
18+
// TODO: Experiment what value gives the best performance.
19+
private const val bufferSize = 8192
20+
21+
private const val header = "z|zlib base64|"
22+
23+
internal fun compress(input: String): String {
24+
val outputStream = ByteArrayOutputStream()
25+
val deflater = Deflater()
26+
deflater.setInput(input.toByteArray(charset("UTF-8")))
27+
deflater.finish()
28+
val buffer = ByteArray(bufferSize)
29+
while (!deflater.finished()) {
30+
val byteCount = deflater.deflate(buffer)
31+
outputStream.write(buffer, 0, byteCount)
32+
}
33+
deflater.end()
34+
outputStream.close()
35+
// The RN bridge currently doesn't support sending byte strings, so we
36+
// have to encode the compressed output as a `String`. To avoid any
37+
// trouble, we use base64 to keep things inside ASCII.
38+
//
39+
// Ultimately our ASCII data seems to end up going to SQLite with size
40+
// no more than about 1 byte/char (presumably the string gets encoded
41+
// as UTF-8 and it's exactly 1 byte/char), so this is pretty OK.
42+
return header + Base64.encodeToString(outputStream.toByteArray(),
43+
Base64.DEFAULT)
44+
}
45+
46+
internal fun decompress(input: String): String {
47+
val inflater = Inflater()
48+
val inputBytes = input.toByteArray(charset("ISO-8859-1"))
49+
inflater.setInput(Base64.decode(inputBytes,
50+
header.length,
51+
inputBytes.size - header.length,
52+
Base64.DEFAULT))
53+
val outputStream = ByteArrayOutputStream()
54+
val buffer = ByteArray(bufferSize)
55+
while (inflater.remaining != 0) {
56+
val byteCount = inflater.inflate(buffer)
57+
outputStream.write(buffer, 0, byteCount)
58+
}
59+
inflater.end()
60+
outputStream.close()
61+
return outputStream.toString("UTF-8")
62+
}
63+
64+
internal class TextCompressionModule(reactContext: ReactApplicationContext?) :
65+
ReactContextBaseJavaModule(reactContext) {
66+
override fun getName(): String = "TextCompressionModule"
67+
68+
override fun getConstants(): Map<String, Any> = hashMapOf("header" to header)
69+
70+
@ReactMethod
71+
fun compress(input: String, promise: Promise) {
72+
try {
73+
promise.resolve(compress(input))
74+
} catch (e: UnsupportedEncodingException) {
75+
promise.reject("UNSUPPORTED_ENCODING_EXCEPTION", e)
76+
} catch (e: IOException) {
77+
promise.reject("IO_EXCEPTION", e)
78+
}
79+
}
80+
81+
@ReactMethod
82+
fun decompress(input: String, promise: Promise) {
83+
try {
84+
promise.resolve(decompress(input))
85+
} catch (e: UnsupportedEncodingException) {
86+
promise.reject("UNSUPPORTED_ENCODING_EXCEPTION", e)
87+
} catch (e: IOException) {
88+
promise.reject("IO_EXCEPTION", e)
89+
} catch (e: DataFormatException) {
90+
promise.reject("DATA_FORMAT_EXCEPTION", e)
91+
}
92+
}
93+
}

android/app/src/main/java/com/zulipmobile/TextCompressionModule.java

Lines changed: 0 additions & 96 deletions
This file was deleted.

src/__tests__/lib/exampleData.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ export const action = Object.freeze({
723723
msg: '',
724724
queue_id: '1',
725725
zulip_feature_level: recentZulipFeatureLevel,
726-
zulip_version: recentZulipVersion.raw(),
726+
zulip_version: recentZulipVersion,
727727

728728
// InitialDataAlertWords
729729
alert_words: [],

src/account-info/ProfileScreen.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ import { tryStopNotifications } from '../notification/notifTokens';
1616
import AccountDetails from './AccountDetails';
1717
import { getRealm } from '../directSelectors';
1818
import { getOwnUser, getOwnUserId } from '../users/userSelectors';
19-
import { getAuth, getIdentity, getZulipFeatureLevel } from '../account/accountsSelectors';
19+
import { getAuth, getAccount, getZulipFeatureLevel } from '../account/accountsSelectors';
2020
import { useNavigation } from '../react-navigation';
2121
import { showConfirmationDialog } from '../utils/info';
2222
import { OfflineNoticePlaceholder } from '../boot/OfflineNoticeProvider';
2323
import { getUserStatus } from '../user-statuses/userStatusesModel';
2424
import SwitchRow from '../common/SwitchRow';
2525
import * as api from '../api';
26+
import { identityOfAccount } from '../account/accountMisc';
2627

2728
const styles = createStyleSheet({
2829
buttonRow: {
@@ -94,7 +95,8 @@ function SwitchAccountButton(props: {||}) {
9495
function LogoutButton(props: {||}) {
9596
const dispatch = useDispatch();
9697
const _ = useContext(TranslationContext);
97-
const identity = useSelector(getIdentity);
98+
const account = useSelector(getAccount);
99+
const identity = identityOfAccount(account);
98100
return (
99101
<ZulipButton
100102
style={styles.button}
@@ -109,7 +111,7 @@ function LogoutButton(props: {||}) {
109111
values: { email: identity.email, realmUrl: identity.realm.toString() },
110112
},
111113
onPressConfirm: () => {
112-
dispatch(tryStopNotifications());
114+
dispatch(tryStopNotifications(account));
113115
dispatch(logout());
114116
},
115117
_,

src/account/AccountList.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ import ViewPlaceholder from '../common/ViewPlaceholder';
88
import AccountItem from './AccountItem';
99

1010
type Props = $ReadOnly<{|
11-
accounts: $ReadOnlyArray<AccountStatus>,
11+
accountStatuses: $ReadOnlyArray<AccountStatus>,
1212
onAccountSelect: number => Promise<void> | void,
1313
onAccountRemove: number => Promise<void> | void,
1414
|}>;
1515

1616
export default function AccountList(props: Props): Node {
17-
const { accounts, onAccountSelect, onAccountRemove } = props;
17+
const { accountStatuses, onAccountSelect, onAccountRemove } = props;
1818

1919
return (
2020
<View>
2121
<FlatList
22-
data={accounts}
22+
data={accountStatuses}
2323
keyExtractor={item => `${item.email}${item.realm.toString()}`}
2424
ItemSeparatorComponent={() => <ViewPlaceholder height={8} />}
2525
renderItem={({ item, index }) => (

src/account/AccountPickScreen.js

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@
22

33
import React, { useContext, useCallback } from 'react';
44
import type { Node } from 'react';
5+
import invariant from 'invariant';
56

67
import * as api from '../api';
78
import { TranslationContext } from '../boot/TranslationProvider';
89
import type { RouteProp } from '../react-navigation';
910
import type { AppNavigationProp } from '../nav/AppNavigator';
1011
import { useGlobalSelector, useGlobalDispatch } from '../react-redux';
11-
import { getAccountStatuses } from '../selectors';
12+
import { getAccountStatuses, getAccountsByIdentity } from '../selectors';
1213
import Centerer from '../common/Centerer';
1314
import ZulipButton from '../common/ZulipButton';
1415
import Logo from '../common/Logo';
1516
import Screen from '../common/Screen';
1617
import ViewPlaceholder from '../common/ViewPlaceholder';
1718
import AccountList from './AccountList';
1819
import { accountSwitch, removeAccount } from '../actions';
19-
import type { ApiResponseServerSettings } from '../api/settings/getServerSettings';
20+
import type { ServerSettings } from '../api/settings/getServerSettings';
2021
import { showConfirmationDialog, showErrorAlert } from '../utils/info';
22+
import { tryStopNotifications } from '../notification/notifTokens';
2123

2224
type Props = $ReadOnly<{|
2325
navigation: AppNavigationProp<'account-pick'>,
@@ -26,33 +28,41 @@ type Props = $ReadOnly<{|
2628

2729
export default function AccountPickScreen(props: Props): Node {
2830
const { navigation } = props;
29-
const accounts = useGlobalSelector(getAccountStatuses);
31+
const accountStatuses = useGlobalSelector(getAccountStatuses);
32+
33+
// In case we need to grab the API for an account (being careful while
34+
// doing so, of course).
35+
const accountsByIdentity = useGlobalSelector(getAccountsByIdentity);
36+
3037
const dispatch = useGlobalDispatch();
3138
const _ = useContext(TranslationContext);
3239

3340
const handleAccountSelect = useCallback(
3441
async (index: number) => {
35-
const { realm, isLoggedIn } = accounts[index];
42+
const { realm, isLoggedIn } = accountStatuses[index];
3643
if (isLoggedIn) {
3744
setTimeout(() => {
3845
dispatch(accountSwitch(index));
3946
});
4047
} else {
4148
try {
42-
const serverSettings: ApiResponseServerSettings = await api.getServerSettings(realm);
49+
const serverSettings: ServerSettings = await api.getServerSettings(realm);
4350
navigation.push('auth', { serverSettings });
4451
} catch {
4552
// TODO: show specific error message from error object
4653
showErrorAlert(_('Failed to connect to server: {realm}', { realm: realm.toString() }));
4754
}
4855
}
4956
},
50-
[accounts, dispatch, navigation, _],
57+
[accountStatuses, dispatch, navigation, _],
5158
);
5259

5360
const handleAccountRemove = useCallback(
5461
(index: number) => {
55-
const { realm, email } = accounts[index];
62+
const { realm, email, isLoggedIn } = accountStatuses[index];
63+
const account = accountsByIdentity({ realm, email });
64+
invariant(account, 'AccountPickScreen: should have account');
65+
5666
showConfirmationDialog({
5767
destructive: true,
5868
title: 'Remove account',
@@ -61,12 +71,19 @@ export default function AccountPickScreen(props: Props): Node {
6171
values: { realmUrl: realm.toString(), email },
6272
},
6373
onPressConfirm: () => {
74+
if (isLoggedIn) {
75+
// Don't delay the removeAccount action by awaiting this
76+
// request: it may take a long time or never succeed, and the
77+
// user expects the account to be removed from the list
78+
// immediately.
79+
dispatch(tryStopNotifications(account));
80+
}
6481
dispatch(removeAccount(index));
6582
},
6683
_,
6784
});
6885
},
69-
[accounts, _, dispatch],
86+
[accountStatuses, accountsByIdentity, _, dispatch],
7087
);
7188

7289
return (
@@ -78,9 +95,9 @@ export default function AccountPickScreen(props: Props): Node {
7895
shouldShowLoadingBanner={false}
7996
>
8097
<Centerer>
81-
{accounts.length === 0 && <Logo />}
98+
{accountStatuses.length === 0 && <Logo />}
8299
<AccountList
83-
accounts={accounts}
100+
accountStatuses={accountStatuses}
84101
onAccountSelect={handleAccountSelect}
85102
onAccountRemove={handleAccountRemove}
86103
/>

src/account/__tests__/accountsReducer-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('accountsReducer', () => {
2626
expect(
2727
accountsReducer(
2828
deepFreeze([account1, account2, account3]),
29-
eg.mkActionRegisterComplete({ zulip_version: newZulipVersion.raw() }),
29+
eg.mkActionRegisterComplete({ zulip_version: newZulipVersion }),
3030
),
3131
).toEqual([{ ...account1, zulipVersion: newZulipVersion }, account2, account3]);
3232
});

src/account/accountsReducer.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ const registerComplete = (state, action) => [
2121
{
2222
...state[0],
2323
userId: action.data.user_id,
24-
zulipFeatureLevel: action.data.zulip_feature_level ?? 0,
25-
zulipVersion: new ZulipVersion(action.data.zulip_version),
24+
zulipFeatureLevel: action.data.zulip_feature_level,
25+
zulipVersion: action.data.zulip_version,
2626
lastDismissedServerPushSetupNotice: action.data.realm_push_notifications_enabled
2727
? null
2828
: state[0].lastDismissedServerPushSetupNotice,

0 commit comments

Comments
 (0)