1
+ import 'dart:convert' ;
2
+ import 'package:drift/drift.dart' ;
3
+
4
+ import 'migration_utils.dart' as utils;
5
+
6
+
7
+ class LegacyDatabase extends GeneratedDatabase {
8
+ LegacyDatabase (super .e);
9
+
10
+ @override
11
+ Iterable <TableInfo <Table ,dynamic >> get allTables => [];
12
+
13
+ @override
14
+ int get schemaVersion => 1 ;
15
+
16
+ Future <List <Map <String , dynamic >>> rawQuery (String query) {
17
+ return customSelect (query).map ((row) => row.data).get ();
18
+ }
19
+
20
+ Future <int > getVersion () async {
21
+ String ? item = await getItem ('reduxPersist:migrations' );
22
+ if (item == null ) {
23
+ return - 1 ;
24
+ }
25
+ var decodedValue = jsonDecode (item);
26
+ var version = decodedValue['version' ] as int ;
27
+ return version;
28
+ }
29
+ // This method is from the legacy RN codebase from
30
+ // src\storage\CompressedAsyncStorage.js and src\storage\AsyncStorage.js
31
+ Future <String ?> getItem (String key) async {
32
+ final query = 'SELECT value FROM keyvalue WHERE key = ?' ;
33
+ final rows = await customSelect (query, variables: [Variable <String >(key)])
34
+ .map ((row) => row.data)
35
+ .get ();
36
+ String ? item = rows.isNotEmpty ? rows[0 ]['value' ] as String : null ;
37
+ if (item == null ) return null ;
38
+ // It's possible that getItem() is called on uncompressed state, for
39
+ // example when a user updates their app from a version without
40
+ // compression to a version with compression. So we need to detect that.
41
+ //
42
+ // We can detect compressed states by inspecting the first few
43
+ // characters of `result`. First, a leading 'z' indicates a
44
+ // "Zulip"-compressed string; otherwise, the string is the only other
45
+ // format we've ever stored, namely uncompressed JSON (which,
46
+ // conveniently, never starts with a 'z').
47
+ //
48
+ // Then, a Zulip-compressed string looks like `z|TRANSFORMS|DATA`, where
49
+ // TRANSFORMS is a space-separated list of the transformations that we
50
+ // applied, in order, to the data to produce DATA and now need to undo.
51
+ // E.g., `zlib base64` means DATA is a base64 encoding of a zlib
52
+ // encoding of the underlying data. We call the "z|TRANSFORMS|" part
53
+ // the "header" of the string.
54
+ if (item.startsWith ('z' )) {
55
+ String itemHeader = '${item .split ('|' ).sublist (0 , 2 ).join ('|' )}|' ;
56
+ if (itemHeader == utils.header) {
57
+ // The string is compressed, so we need to decompress it.
58
+ String decompressedString = utils.decompress (item);
59
+ return decompressedString;
60
+ } else {
61
+ // Panic! If we are confronted with an unknown format, there is
62
+ // nothing we can do to save the situation. Log an error and ignore
63
+ // the data. This error should not happen unless a user downgrades
64
+ // their version of the app.
65
+ final err = Exception (
66
+ 'No decompression module found for format $itemHeader ' );
67
+ throw err;
68
+ }
69
+ }
70
+ // Uncompressed state
71
+ return item;
72
+
73
+ }
74
+ }
75
+
76
+ class LegacyAppMigrations {
77
+ LegacyAppMigrations ();
78
+
79
+ /// This method should return the json data of the account in the latest version
80
+ /// of migrations or null if the data can't be migrated.
81
+ static Map <String , dynamic >? applyAccountMigrations (Map <String , dynamic > json, int version) {
82
+ if (version < 9 ) {
83
+ // json['ackedPushToken'] should be set to null
84
+ json['ackedPushToken' ] = null ;
85
+ }
86
+
87
+ if (version < 11 ) {
88
+ // removes multiple trailing slashes from json['realm'].
89
+ json['realm' ] = json['realm' ].replaceAll (RegExp (r'/+$' ), '' );
90
+ }
91
+
92
+ if (version < 12 ) {
93
+ // Add zulipVersion to accounts.
94
+ json['zulipVersion' ] = null ;
95
+ }
96
+
97
+ // if (version < 13) {
98
+ // this should convert json['zulipVersion'] from `string | null` to `ZulipVersion | null`
99
+ // but we already have it as `string | null` in this app so no point of
100
+ // doing this then making it string back
101
+ // }
102
+
103
+ if (version < 14 ) {
104
+ // Add zulipFeatureLevel to accounts.
105
+ json['zulipFeatureLevel' ] = null ;
106
+ }
107
+
108
+ if (version < 15 ) {
109
+ // convert json['realm'] from string to Uri.
110
+ json['realm' ] = Uri .parse (json['realm' ] as String );
111
+ }
112
+
113
+ if (version < 27 ) {
114
+ // Remove accounts with "in-progress" login state (empty json['email'])
115
+ // make all fields null
116
+ if (json['email' ] == null || json['email' ] == '' ) {
117
+ return null ;
118
+ }
119
+ }
120
+
121
+ if (version < 33 ) {
122
+ // Add userId to accounts.
123
+ json['userId' ] = null ;
124
+ }
125
+
126
+ if (version < 36 ) {
127
+ // Add lastDismissedServerPushSetupNotice to accounts.
128
+ json['lastDismissedServerPushSetupNotice' ] = null ;
129
+
130
+ }
131
+
132
+ if (version < 58 ) {
133
+ const requiredKeys = [
134
+ 'realm' ,
135
+ 'apiKey' ,
136
+ 'email' ,
137
+ 'userId' ,
138
+ 'zulipVersion' ,
139
+ 'zulipFeatureLevel' ,
140
+ 'ackedPushToken' ,
141
+ 'lastDismissedServerPushSetupNotice' ,
142
+ ];
143
+ bool hasAllRequiredKeys = requiredKeys.every ((key) => json.containsKey (key));
144
+ if (! hasAllRequiredKeys) {
145
+ return null ;
146
+ }
147
+ }
148
+
149
+ if (version < 62 ) {
150
+ // Add silenceServerPushSetupWarnings to accounts.
151
+ json['silenceServerPushSetupWarnings' ] = false ;
152
+ }
153
+
154
+ if (version < 66 ) {
155
+ // Add lastDismissedServerNotifsExpiringBanner to accounts.
156
+ json['lastDismissedServerNotifsExpiringBanner' ] = null ;
157
+ }
158
+ return json;
159
+ }
160
+
161
+ static Map <String , dynamic >? applySettingMigrations (Map <String ,dynamic > json, int version) {
162
+ if (version < 10 ) {
163
+ // Convert old locale names to new, more-specific locale names.
164
+ final newLocaleNames = {
165
+ 'zh' : 'zh-Hans' ,
166
+ 'id' : 'id-ID' ,
167
+ };
168
+ if (newLocaleNames.containsKey (json['locale' ])) {
169
+ json['locale' ] = newLocaleNames[json['locale' ]];
170
+ }
171
+ }
172
+
173
+ if (version < 26 ) {
174
+ // Rename locale `id-ID` back to `id`.
175
+ if (json['locale' ] == 'id-ID' ) {
176
+ json['locale' ] = 'id' ;
177
+ }
178
+ }
179
+
180
+ if (version < 28 ) {
181
+ // Add "open links with in-app browser" setting.
182
+ json['browser' ] = 'default' ;
183
+ }
184
+
185
+ if (version < 30 ) {
186
+ // Use valid language tag for Portuguese (Portugal).
187
+ if (json['locale' ] == 'pt_PT' ) {
188
+ json['locale' ] = 'pt-PT' ;
189
+ }
190
+ }
191
+
192
+ if (version < 31 ) {
193
+ // rename json['locale'] to json['language'].
194
+ json['language' ] = json['locale' ];
195
+ json.remove ('locale' );
196
+ }
197
+
198
+ if (version < 32 ) {
199
+ // Switch to zh-TW as a language option instead of zh-Hant.
200
+ if (json['language' ] == 'zh-Hant' ) {
201
+ json['language' ] = 'zh-TW' ;
202
+ }
203
+ }
204
+
205
+ if (version < 37 ) {
206
+ // Adds `doNotMarkMessagesAsRead` to `settings`. If the property is missing, it defaults to `false`.
207
+ json['doNotMarkMessagesAsRead' ] = json['doNotMarkMessagesAsRead' ] ?? false ;
208
+ }
209
+
210
+ if (version < 52 ) {
211
+ // Change boolean doNotMarkMessagesAsRead to enum markMessagesReadOnScroll.
212
+ if (json['doNotMarkMessagesAsRead' ] == true ) {
213
+ json['markMessagesReadOnScroll' ] = 'never' ;
214
+ } else {
215
+ json['markMessagesReadOnScroll' ] = 'always' ;
216
+ }
217
+ json.remove ('doNotMarkMessagesAsRead' );
218
+ }
219
+
220
+ return json;
221
+ }
222
+ }
0 commit comments