|
1 | 1 | import 'dart:convert';
|
| 2 | +import 'dart:io'; |
2 | 3 |
|
| 4 | +import 'package:drift/native.dart'; |
3 | 5 | import 'package:flutter/foundation.dart';
|
| 6 | +import 'package:path/path.dart' as p; |
| 7 | +import 'package:path_provider/path_provider.dart'; |
4 | 8 |
|
5 | 9 | import '../api/core.dart';
|
6 | 10 | import '../api/model/events.dart';
|
7 | 11 | import '../api/model/initial_snapshot.dart';
|
8 | 12 | import '../api/model/model.dart';
|
9 | 13 | import '../api/route/events.dart';
|
10 | 14 | import '../api/route/messages.dart';
|
11 |
| -import '../credential_fixture.dart' as credentials; |
12 | 15 | import 'database.dart';
|
13 | 16 | import 'message_list.dart';
|
14 | 17 |
|
@@ -110,8 +113,6 @@ abstract class GlobalStore extends ChangeNotifier {
|
110 | 113 |
|
111 | 114 | Account? getAccount(int id) => _accounts[id];
|
112 | 115 |
|
113 |
| - // TODO(#13): rewrite these setters/mutators with a database |
114 |
| - |
115 | 116 | /// Add an account to the store, returning its assigned account ID.
|
116 | 117 | Future<int> insertAccount(AccountsCompanion data) async {
|
117 | 118 | final account = await doInsertAccount(data);
|
@@ -207,58 +208,70 @@ class PerAccountStore extends ChangeNotifier {
|
207 | 208 | }
|
208 | 209 | }
|
209 | 210 |
|
| 211 | +/// A [GlobalStore] that uses a live server and live, persistent local database. |
| 212 | +/// |
| 213 | +/// The underlying data store is an [AppDatabase] corresponding to a SQLite |
| 214 | +/// database file in the app's persistent storage on the device. |
| 215 | +/// |
| 216 | +/// The per-account stores will be instances of [LivePerAccountStore], |
| 217 | +/// with data loaded through [LiveApiConnection]. |
210 | 218 | class LiveGlobalStore extends GlobalStore {
|
211 |
| - LiveGlobalStore._({required super.accounts}) : super(); |
212 |
| - |
213 |
| - // For convenience, a number we won't use as an ID in the database table. |
214 |
| - static const fixtureAccountId = -1; |
| 219 | + LiveGlobalStore._({ |
| 220 | + required AppDatabase db, |
| 221 | + required super.accounts, |
| 222 | + }) : _db = db; |
215 | 223 |
|
216 | 224 | // We keep the API simple and synchronous for the bulk of the app's code
|
217 | 225 | // by doing this loading up front before constructing a [GlobalStore].
|
218 | 226 | static Future<GlobalStore> load() async {
|
219 |
| - final accounts = {fixtureAccountId: _fixtureAccount}; |
220 |
| - return LiveGlobalStore._(accounts: accounts); |
| 227 | + final db = AppDatabase(NativeDatabase.createInBackground(await _dbFile())); |
| 228 | + final accounts = await db.select(db.accounts).get(); |
| 229 | + return LiveGlobalStore._( |
| 230 | + db: db, |
| 231 | + accounts: Map.fromEntries(accounts.map((a) => MapEntry(a.id, a))), |
| 232 | + ); |
| 233 | + } |
| 234 | + |
| 235 | + /// The file path to use for the app database. |
| 236 | + static Future<File> _dbFile() async { |
| 237 | + // What directory should we use? |
| 238 | + // path_provider's getApplicationSupportDirectory: |
| 239 | + // on Android, -> Flutter's PathUtils.getFilesDir -> https://developer.android.com/reference/android/content/Context#getFilesDir() |
| 240 | + // -> empirically /data/data/com.zulip.flutter/files/ |
| 241 | + // on iOS, -> "Library/Application Support" via https://developer.apple.com/documentation/foundation/nssearchpathdirectory/nsapplicationsupportdirectory |
| 242 | + // on Linux, -> "${XDG_DATA_HOME:-~/.local/share}/com.zulip.flutter/" |
| 243 | + // All seem reasonable. |
| 244 | + // path_provider's getApplicationDocumentsDirectory: |
| 245 | + // on Android, -> Flutter's PathUtils.getDataDirectory -> https://developer.android.com/reference/android/content/Context#getDir(java.lang.String,%20int) |
| 246 | + // with https://developer.android.com/reference/android/content/Context#MODE_PRIVATE |
| 247 | + // on iOS, "Document directory" via https://developer.apple.com/documentation/foundation/nssearchpathdirectory/nsdocumentdirectory |
| 248 | + // on Linux, -> `xdg-user-dir DOCUMENTS` -> e.g. ~/Documents |
| 249 | + // That Linux answer is definitely not a fit. Harder to tell about the rest. |
| 250 | + final dir = await getApplicationSupportDirectory(); |
| 251 | + return File(p.join(dir.path, 'zulip.db')); |
221 | 252 | }
|
222 | 253 |
|
| 254 | + final AppDatabase _db; |
| 255 | + |
223 | 256 | @override
|
224 | 257 | Future<PerAccountStore> loadPerAccount(Account account) {
|
225 | 258 | return LivePerAccountStore.load(account);
|
226 | 259 | }
|
227 | 260 |
|
228 |
| - int _nextAccountId = 1; |
229 |
| - |
230 | 261 | @override
|
231 | 262 | Future<Account> doInsertAccount(AccountsCompanion data) async {
|
232 |
| - final accountId = _nextAccountId; |
233 |
| - _nextAccountId++; |
234 |
| - return Account( |
235 |
| - id: accountId, |
236 |
| - realmUrl: data.realmUrl.value, |
237 |
| - userId: data.userId.value, |
238 |
| - email: data.email.value, |
239 |
| - apiKey: data.apiKey.value, |
240 |
| - zulipFeatureLevel: data.zulipFeatureLevel.value, |
241 |
| - zulipVersion: data.zulipVersion.value, |
242 |
| - zulipMergeBase: data.zulipMergeBase.value, |
243 |
| - ); |
| 263 | + final accountId = await _db.createAccount(data); |
| 264 | + // We can *basically* predict what the Account will contain |
| 265 | + // based on the AccountsCompanion and the account ID. But |
| 266 | + // if we did that and then there was some subtle case where we |
| 267 | + // didn't match the database's behavior, that'd be a nasty bug. |
| 268 | + // This isn't a hot path, so just make the extra query. |
| 269 | + return await (_db.select(_db.accounts) // TODO perhaps put this logic in AppDatabase |
| 270 | + ..where((a) => a.id.equals(accountId)) |
| 271 | + ).getSingle(); |
244 | 272 | }
|
245 | 273 | }
|
246 | 274 |
|
247 |
| -/// A scaffolding hack for while prototyping. |
248 |
| -/// |
249 |
| -/// See "Server credentials" in the project README for how to fill in the |
250 |
| -/// `credential_fixture.dart` file this requires. |
251 |
| -final Account _fixtureAccount = Account( |
252 |
| - id: LiveGlobalStore.fixtureAccountId, |
253 |
| - realmUrl: Uri.parse(credentials.realmUrl), |
254 |
| - email: credentials.email, |
255 |
| - apiKey: credentials.apiKey, |
256 |
| - userId: credentials.userId, |
257 |
| - zulipFeatureLevel: 169, |
258 |
| - zulipVersion: '6.0-1235-g061f1dc43b', |
259 |
| - zulipMergeBase: '6.0-1235-g061f1dc43b', |
260 |
| -); |
261 |
| - |
262 | 275 | /// A [PerAccountStore] which polls an event queue to stay up to date.
|
263 | 276 | class LivePerAccountStore extends PerAccountStore {
|
264 | 277 | LivePerAccountStore.fromInitialSnapshot({
|
|
0 commit comments