mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-03-03 08:46:46 +00:00
remove old database
This commit is contained in:
parent
de41111194
commit
eb70d7119f
35 changed files with 1 additions and 13635 deletions
|
|
@ -11,5 +11,4 @@ targets:
|
|||
options:
|
||||
databases:
|
||||
twonly_db: lib/src/database/twonly.db.dart
|
||||
twonly_database: lib/src/database/twonly_database_old.dart
|
||||
schema_dir: lib/src/database/schemas
|
||||
|
|
@ -20,7 +20,6 @@ import 'package:twonly/src/views/home.view.dart';
|
|||
import 'package:twonly/src/views/onboarding/onboarding.view.dart';
|
||||
import 'package:twonly/src/views/onboarding/register.view.dart';
|
||||
import 'package:twonly/src/views/settings/backup/setup_backup.view.dart';
|
||||
import 'package:twonly/src/views/updates/62_database_migration.view.dart';
|
||||
|
||||
class App extends StatefulWidget {
|
||||
const App({super.key});
|
||||
|
|
@ -187,7 +186,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
late Widget child;
|
||||
|
||||
if (_showDatabaseMigration) {
|
||||
child = const DatabaseMigrationView();
|
||||
child = const Center(child: Text('Please reinstall twonly.'));
|
||||
} else if (_isUserCreated) {
|
||||
if (gUser.twonlySafeBackup == null && !_skipBackup && kReleaseMode) {
|
||||
child = SetupBackupView(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:drift/drift.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/database/twonly_database_old.dart' as old;
|
||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
|
|
@ -163,22 +162,6 @@ String substringBy(String string, int maxLength) {
|
|||
return string;
|
||||
}
|
||||
|
||||
String getContactDisplayNameOld(old.Contact user) {
|
||||
var name = user.username;
|
||||
if (user.nickName != null && user.nickName != '') {
|
||||
name = user.nickName!;
|
||||
} else if (user.displayName != null) {
|
||||
name = user.displayName!;
|
||||
}
|
||||
if (user.deleted) {
|
||||
name = applyStrikethrough(name);
|
||||
}
|
||||
if (name.length > 12) {
|
||||
return '${name.substring(0, 12)}...';
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
String applyStrikethrough(String text) {
|
||||
return text.split('').map((char) => '$char\u0336').join();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/signal_contact_prekey.table.dart';
|
||||
import 'package:twonly/src/database/tables/signal_contact_signed_prekey.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
part 'signal.dao.g.dart';
|
||||
|
||||
@DriftAccessor(
|
||||
tables: [
|
||||
SignalContactPreKeys,
|
||||
SignalContactSignedPreKeys,
|
||||
],
|
||||
)
|
||||
class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
|
||||
// this constructor is required so that the main database can create an instance
|
||||
// of this object.
|
||||
// ignore: matching_super_parameters
|
||||
SignalDao(super.db);
|
||||
Future<void> deleteAllByContactId(int contactId) async {
|
||||
await (delete(signalContactPreKeys)
|
||||
..where((t) => t.contactId.equals(contactId)))
|
||||
.go();
|
||||
await (delete(signalContactSignedPreKeys)
|
||||
..where((t) => t.contactId.equals(contactId)))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<void> deleteAllPreKeysByContactId(int contactId) async {
|
||||
await (delete(signalContactPreKeys)
|
||||
..where((t) => t.contactId.equals(contactId)))
|
||||
.go();
|
||||
}
|
||||
|
||||
// 1: Count the number of pre-keys by contact ID
|
||||
Future<int> countPreKeysByContactId(int contactId) {
|
||||
return (select(signalContactPreKeys)
|
||||
..where((tbl) => tbl.contactId.equals(contactId)))
|
||||
.get()
|
||||
.then((rows) => rows.length);
|
||||
}
|
||||
|
||||
// 2: Pop a pre-key by contact ID
|
||||
Future<SignalContactPreKey?> popPreKeyByContactId(int contactId) async {
|
||||
final preKey =
|
||||
await ((select(signalContactPreKeys)..where((tbl) => tbl.contactId.equals(contactId)))
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
|
||||
if (preKey != null) {
|
||||
// remove the pre key...
|
||||
await (delete(signalContactPreKeys)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.contactId.equals(contactId) &
|
||||
tbl.preKeyId.equals(preKey.preKeyId),
|
||||
))
|
||||
.go();
|
||||
return preKey;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3: Insert multiple pre-keys
|
||||
Future<void> insertPreKeys(
|
||||
List<SignalContactPreKeysCompanion> preKeys,
|
||||
) async {
|
||||
for (final preKey in preKeys) {
|
||||
try {
|
||||
await into(signalContactPreKeys).insert(preKey);
|
||||
} catch (e) {
|
||||
Log.error('$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4: Get signed pre-key by contact ID
|
||||
Future<SignalContactSignedPreKey?> getSignedPreKeyByContactId(int contactId) {
|
||||
return (select(signalContactSignedPreKeys)
|
||||
..where((tbl) => tbl.contactId.equals(contactId)))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
// 5: Insert or update signed pre-key by contact ID
|
||||
Future<void> insertOrUpdateSignedPreKeyByContactId(
|
||||
SignalContactSignedPreKeysCompanion signedPreKey,
|
||||
) async {
|
||||
await (delete(signalContactSignedPreKeys)
|
||||
..where((t) => t.contactId.equals(signedPreKey.contactId.value)))
|
||||
.go();
|
||||
await into(signalContactSignedPreKeys).insert(signedPreKey);
|
||||
}
|
||||
|
||||
Future<void> purgePreKeysFromContact(int contactId) async {
|
||||
await (delete(signalContactPreKeys)
|
||||
..where(
|
||||
(t) => (t.contactId.equals(contactId)),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<void> purgeOutDatedPreKeys() async {
|
||||
// other pre keys are valid 100 days
|
||||
await (delete(signalContactPreKeys)
|
||||
..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
clock.now().subtract(
|
||||
const Duration(days: 100),
|
||||
),
|
||||
)),
|
||||
))
|
||||
.go();
|
||||
// own pre keys are valid for 180 days
|
||||
await (delete(twonlyDB.signalPreKeyStores)
|
||||
..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
clock.now().subtract(
|
||||
const Duration(days: 365),
|
||||
),
|
||||
)),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,42 +0,0 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
class Contacts extends Table {
|
||||
IntColumn get userId => integer()();
|
||||
|
||||
TextColumn get username => text().unique()();
|
||||
TextColumn get displayName => text().nullable()();
|
||||
TextColumn get nickName => text().nullable()();
|
||||
TextColumn get avatarSvg => text().nullable()();
|
||||
|
||||
IntColumn get myAvatarCounter => integer().withDefault(const Constant(0))();
|
||||
|
||||
BoolColumn get accepted => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get requested => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get blocked => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get verified => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get archived => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get pinned => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get deleted => boolean().withDefault(const Constant(false))();
|
||||
|
||||
BoolColumn get alsoBestFriend =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
|
||||
IntColumn get deleteMessagesAfterXMinutes =>
|
||||
integer().withDefault(const Constant(60 * 24))();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
IntColumn get totalMediaCounter => integer().withDefault(const Constant(0))();
|
||||
|
||||
DateTimeColumn get lastMessageSend => dateTime().nullable()();
|
||||
DateTimeColumn get lastMessageReceived => dateTime().nullable()();
|
||||
DateTimeColumn get lastFlameCounterChange => dateTime().nullable()();
|
||||
DateTimeColumn get lastFlameSync => dateTime().nullable()();
|
||||
DateTimeColumn get lastMessageExchange =>
|
||||
dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
IntColumn get flameCounter => integer().withDefault(const Constant(0))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {userId};
|
||||
}
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
import 'dart:convert';
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
enum UploadState {
|
||||
pending,
|
||||
readyToUpload,
|
||||
uploadTaskStarted,
|
||||
receiverNotified,
|
||||
}
|
||||
|
||||
@DataClassName('MediaUpload')
|
||||
class MediaUploads extends Table {
|
||||
IntColumn get mediaUploadId => integer().autoIncrement()();
|
||||
TextColumn get state =>
|
||||
textEnum<UploadState>().withDefault(Constant(UploadState.pending.name))();
|
||||
|
||||
TextColumn get metadata =>
|
||||
text().map(const MediaUploadMetadataConverter()).nullable()();
|
||||
|
||||
/// exists in UploadState.addedToMessagesDb
|
||||
TextColumn get messageIds => text().map(IntListTypeConverter()).nullable()();
|
||||
|
||||
TextColumn get encryptionData =>
|
||||
text().map(const MediaEncryptionDataConverter()).nullable()();
|
||||
}
|
||||
|
||||
// --- state ----
|
||||
|
||||
class MediaUploadMetadata {
|
||||
MediaUploadMetadata();
|
||||
factory MediaUploadMetadata.fromJson(Map<String, dynamic> json) {
|
||||
return MediaUploadMetadata()
|
||||
..contactIds = List<int>.from(json['contactIds'] as Iterable<dynamic>)
|
||||
..isRealTwonly = json['isRealTwonly'] as bool
|
||||
..isVideo = json['isVideo'] as bool
|
||||
..mirrorVideo = json['mirrorVideo'] as bool
|
||||
..maxShowTime = json['maxShowTime'] as int
|
||||
..maxShowTime = json['maxShowTime'] as int
|
||||
..messageSendAt = DateTime.parse(json['messageSendAt'] as String);
|
||||
}
|
||||
|
||||
late List<int> contactIds;
|
||||
late bool isRealTwonly;
|
||||
late int maxShowTime;
|
||||
late DateTime messageSendAt;
|
||||
late bool isVideo;
|
||||
late bool mirrorVideo;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'contactIds': contactIds,
|
||||
'isRealTwonly': isRealTwonly,
|
||||
'mirrorVideo': mirrorVideo,
|
||||
'maxShowTime': maxShowTime,
|
||||
'isVideo': isVideo,
|
||||
'messageSendAt': messageSendAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MediaEncryptionData {
|
||||
MediaEncryptionData();
|
||||
|
||||
factory MediaEncryptionData.fromJson(Map<String, dynamic> json) {
|
||||
return MediaEncryptionData()
|
||||
..sha2Hash = List<int>.from(json['sha2Hash'] as Iterable<dynamic>)
|
||||
..encryptionKey =
|
||||
List<int>.from(json['encryptionKey'] as Iterable<dynamic>)
|
||||
..encryptionMac =
|
||||
List<int>.from(json['encryptionMac'] as Iterable<dynamic>)
|
||||
..encryptionNonce =
|
||||
List<int>.from(json['encryptionNonce'] as Iterable<dynamic>);
|
||||
}
|
||||
late List<int> sha2Hash;
|
||||
late List<int> encryptionKey;
|
||||
late List<int> encryptionMac;
|
||||
late List<int> encryptionNonce;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'sha2Hash': sha2Hash,
|
||||
'encryptionKey': encryptionKey,
|
||||
'encryptionMac': encryptionMac,
|
||||
'encryptionNonce': encryptionNonce,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// --- converters ----
|
||||
|
||||
class IntListTypeConverter extends TypeConverter<List<int>, String> {
|
||||
@override
|
||||
List<int> fromSql(String fromDb) {
|
||||
return List<int>.from(jsonDecode(fromDb) as Iterable<dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(List<int> value) {
|
||||
return json.encode(value);
|
||||
}
|
||||
}
|
||||
|
||||
class MediaUploadMetadataConverter
|
||||
extends TypeConverter<MediaUploadMetadata, String>
|
||||
with JsonTypeConverter2<MediaUploadMetadata, String, Map<String, Object?>> {
|
||||
const MediaUploadMetadataConverter();
|
||||
|
||||
@override
|
||||
MediaUploadMetadata fromSql(String fromDb) {
|
||||
return fromJson(json.decode(fromDb) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(MediaUploadMetadata value) {
|
||||
return json.encode(toJson(value));
|
||||
}
|
||||
|
||||
@override
|
||||
MediaUploadMetadata fromJson(Map<String, Object?> json) {
|
||||
return MediaUploadMetadata.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson(MediaUploadMetadata value) {
|
||||
return value.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
class MediaEncryptionDataConverter
|
||||
extends TypeConverter<MediaEncryptionData, String>
|
||||
with JsonTypeConverter2<MediaEncryptionData, String, Map<String, Object?>> {
|
||||
const MediaEncryptionDataConverter();
|
||||
|
||||
@override
|
||||
MediaEncryptionData fromSql(String fromDb) {
|
||||
return fromJson(json.decode(fromDb) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(MediaEncryptionData value) {
|
||||
return json.encode(toJson(value));
|
||||
}
|
||||
|
||||
@override
|
||||
MediaEncryptionData fromJson(Map<String, Object?> json) {
|
||||
return MediaEncryptionData.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson(MediaEncryptionData value) {
|
||||
return value.toJson();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/src/database/tables_old/contacts_table.dart';
|
||||
import 'package:twonly/src/database/tables_old/messages_table.dart';
|
||||
|
||||
@DataClassName('MessageRetransmission')
|
||||
class MessageRetransmissions extends Table {
|
||||
IntColumn get retransmissionId => integer().autoIncrement()();
|
||||
IntColumn get contactId =>
|
||||
integer().references(Contacts, #userId, onDelete: KeyAction.cascade)();
|
||||
|
||||
IntColumn get messageId => integer()
|
||||
.nullable()
|
||||
.references(Messages, #messageId, onDelete: KeyAction.cascade)();
|
||||
|
||||
BlobColumn get plaintextContent => blob()();
|
||||
BlobColumn get pushData => blob().nullable()();
|
||||
BlobColumn get encryptedHash => blob().nullable()();
|
||||
|
||||
IntColumn get retryCount => integer().withDefault(const Constant(0))();
|
||||
DateTimeColumn get lastRetry => dateTime().nullable()();
|
||||
|
||||
DateTimeColumn get acknowledgeByServerAt => dateTime().nullable()();
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/src/database/tables_old/contacts_table.dart';
|
||||
|
||||
enum MessageKind {
|
||||
textMessage,
|
||||
storedMediaFile,
|
||||
reopenedMedia,
|
||||
media,
|
||||
contactRequest,
|
||||
profileChange,
|
||||
rejectRequest,
|
||||
acceptRequest,
|
||||
flameSync,
|
||||
opened,
|
||||
ack,
|
||||
pushKey,
|
||||
requestPushKey,
|
||||
receiveMediaError,
|
||||
signalDecryptError
|
||||
}
|
||||
|
||||
enum DownloadState {
|
||||
pending,
|
||||
downloading,
|
||||
downloaded,
|
||||
}
|
||||
|
||||
enum MediaRetransmitting {
|
||||
none,
|
||||
requested,
|
||||
retransmitted,
|
||||
}
|
||||
|
||||
@DataClassName('Message')
|
||||
class Messages extends Table {
|
||||
IntColumn get contactId => integer().references(Contacts, #userId)();
|
||||
|
||||
IntColumn get messageId => integer().autoIncrement()();
|
||||
IntColumn get messageOtherId => integer().nullable()();
|
||||
|
||||
IntColumn get mediaUploadId => integer().nullable()();
|
||||
IntColumn get mediaDownloadId => integer().nullable()();
|
||||
|
||||
IntColumn get responseToMessageId => integer().nullable()();
|
||||
IntColumn get responseToOtherMessageId => integer().nullable()();
|
||||
|
||||
BoolColumn get acknowledgeByUser =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get mediaStored => boolean().withDefault(const Constant(false))();
|
||||
|
||||
IntColumn get downloadState => intEnum<DownloadState>()
|
||||
.withDefault(Constant(DownloadState.downloaded.index))();
|
||||
|
||||
BoolColumn get acknowledgeByServer =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
|
||||
BoolColumn get errorWhileSending =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
TextColumn get mediaRetransmissionState => textEnum<MediaRetransmitting>()
|
||||
.withDefault(Constant(MediaRetransmitting.none.name))();
|
||||
|
||||
TextColumn get kind => textEnum<MessageKind>()();
|
||||
TextColumn get contentJson => text().nullable()();
|
||||
|
||||
DateTimeColumn get openedAt => dateTime().nullable()();
|
||||
DateTimeColumn get sendAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('SignalContactPreKey')
|
||||
class SignalContactPreKeys extends Table {
|
||||
IntColumn get contactId => integer()();
|
||||
IntColumn get preKeyId => integer()();
|
||||
BlobColumn get preKey => blob()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
@override
|
||||
Set<Column> get primaryKey => {contactId, preKeyId};
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('SignalContactSignedPreKey')
|
||||
class SignalContactSignedPreKeys extends Table {
|
||||
IntColumn get contactId => integer()();
|
||||
IntColumn get signedPreKeyId => integer()();
|
||||
BlobColumn get signedPreKey => blob()();
|
||||
BlobColumn get signedPreKeySignature => blob()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
@override
|
||||
Set<Column> get primaryKey => {contactId};
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('SignalIdentityKeyStore')
|
||||
class SignalIdentityKeyStores extends Table {
|
||||
IntColumn get deviceId => integer()();
|
||||
TextColumn get name => text()();
|
||||
BlobColumn get identityKey => blob()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {deviceId, name};
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('SignalPreKeyStore')
|
||||
class SignalPreKeyStores extends Table {
|
||||
IntColumn get preKeyId => integer()();
|
||||
BlobColumn get preKey => blob()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {preKeyId};
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('SignalSenderKeyStore')
|
||||
class SignalSenderKeyStores extends Table {
|
||||
TextColumn get senderKeyName => text()();
|
||||
BlobColumn get senderKey => blob()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {senderKeyName};
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('SignalSessionStore')
|
||||
class SignalSessionStores extends Table {
|
||||
IntColumn get deviceId => integer()();
|
||||
TextColumn get name => text()();
|
||||
BlobColumn get sessionRecord => blob()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {deviceId, name};
|
||||
}
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart'
|
||||
show DriftNativeOptions, driftDatabase;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/database/tables_old/contacts_table.dart';
|
||||
import 'package:twonly/src/database/tables_old/media_uploads_table.dart';
|
||||
import 'package:twonly/src/database/tables_old/message_retransmissions.dart';
|
||||
import 'package:twonly/src/database/tables_old/messages_table.dart';
|
||||
import 'package:twonly/src/database/tables_old/signal_contact_prekey_table.dart';
|
||||
import 'package:twonly/src/database/tables_old/signal_contact_signed_prekey_table.dart';
|
||||
import 'package:twonly/src/database/tables_old/signal_identity_key_store_table.dart';
|
||||
import 'package:twonly/src/database/tables_old/signal_pre_key_store_table.dart';
|
||||
import 'package:twonly/src/database/tables_old/signal_sender_key_store_table.dart';
|
||||
import 'package:twonly/src/database/tables_old/signal_session_store_table.dart';
|
||||
import 'package:twonly/src/database/twonly_database_old.steps.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
part 'twonly_database_old.g.dart';
|
||||
|
||||
// You can then create a database class that includes this table
|
||||
@DriftDatabase(
|
||||
tables: [
|
||||
Contacts,
|
||||
Messages,
|
||||
MediaUploads,
|
||||
SignalIdentityKeyStores,
|
||||
SignalPreKeyStores,
|
||||
SignalSenderKeyStores,
|
||||
SignalSessionStores,
|
||||
SignalContactPreKeys,
|
||||
SignalContactSignedPreKeys,
|
||||
MessageRetransmissions,
|
||||
],
|
||||
)
|
||||
class TwonlyDatabaseOld extends _$TwonlyDatabaseOld {
|
||||
TwonlyDatabaseOld([QueryExecutor? e])
|
||||
: super(
|
||||
e ?? _openConnection(),
|
||||
);
|
||||
|
||||
// ignore: matching_super_parameters
|
||||
TwonlyDatabaseOld.forTesting(DatabaseConnection super.connection);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 17;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
name: 'twonly_database',
|
||||
native: const DriftNativeOptions(
|
||||
databaseDirectory: getApplicationSupportDirectory,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
beforeOpen: (details) async {
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
onUpgrade: stepByStep(
|
||||
from1To2: (m, schema) async {
|
||||
await m.addColumn(schema.messages, schema.messages.errorWhileSending);
|
||||
},
|
||||
from2To3: (m, schema) async {
|
||||
await m.addColumn(schema.contacts, schema.contacts.archived);
|
||||
await m.addColumn(
|
||||
schema.contacts,
|
||||
schema.contacts.deleteMessagesAfterXMinutes,
|
||||
);
|
||||
},
|
||||
from3To4: (m, schema) async {
|
||||
await m.createTable(schema.mediaUploads);
|
||||
await m.alterTable(
|
||||
TableMigration(
|
||||
schema.mediaUploads,
|
||||
columnTransformer: {
|
||||
schema.mediaUploads.metadata:
|
||||
schema.mediaUploads.metadata.cast<String>(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
from4To5: (m, schema) async {
|
||||
await m.createTable(schema.mediaDownloads);
|
||||
await m.addColumn(schema.messages, schema.messages.mediaDownloadId);
|
||||
await m.addColumn(schema.messages, schema.messages.mediaUploadId);
|
||||
},
|
||||
from5To6: (m, schema) async {
|
||||
await m.addColumn(schema.messages, schema.messages.mediaStored);
|
||||
},
|
||||
from6To7: (m, schema) async {
|
||||
await m.addColumn(schema.contacts, schema.contacts.pinned);
|
||||
},
|
||||
from7To8: (m, schema) async {
|
||||
await m.addColumn(schema.contacts, schema.contacts.alsoBestFriend);
|
||||
await m.addColumn(schema.contacts, schema.contacts.lastFlameSync);
|
||||
},
|
||||
from8To9: (m, schema) async {
|
||||
await m.alterTable(
|
||||
TableMigration(
|
||||
schema.mediaUploads,
|
||||
columnTransformer: {
|
||||
schema.mediaUploads.metadata:
|
||||
schema.mediaUploads.metadata.cast<String>(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
from9To10: (m, schema) async {
|
||||
await m.createTable(schema.signalContactPreKeys);
|
||||
await m.createTable(schema.signalContactSignedPreKeys);
|
||||
await m.addColumn(schema.contacts, schema.contacts.deleted);
|
||||
},
|
||||
from10To11: (m, schema) async {
|
||||
await m.createTable(schema.messageRetransmissions);
|
||||
},
|
||||
from11To12: (m, schema) async {
|
||||
await m.addColumn(
|
||||
schema.messageRetransmissions,
|
||||
schema.messageRetransmissions.willNotGetACKByUser,
|
||||
);
|
||||
},
|
||||
from12To13: (m, schema) async {
|
||||
await m.dropColumn(
|
||||
schema.messageRetransmissions,
|
||||
'will_not_get_a_c_k_by_user',
|
||||
);
|
||||
},
|
||||
from13To14: (m, schema) async {
|
||||
await m.addColumn(
|
||||
schema.messageRetransmissions,
|
||||
schema.messageRetransmissions.encryptedHash,
|
||||
);
|
||||
},
|
||||
from14To15: (m, schema) async {
|
||||
await m.dropColumn(schema.mediaUploads, 'upload_tokens');
|
||||
await m.dropColumn(schema.mediaUploads, 'already_notified');
|
||||
await m.addColumn(
|
||||
schema.messages,
|
||||
schema.messages.mediaRetransmissionState,
|
||||
);
|
||||
},
|
||||
from15To16: (m, schema) async {
|
||||
await m.deleteTable('media_downloads');
|
||||
},
|
||||
from16To17: (m, schema) async {
|
||||
await m.addColumn(
|
||||
schema.messageRetransmissions,
|
||||
schema.messageRetransmissions.lastRetry,
|
||||
);
|
||||
await m.addColumn(
|
||||
schema.messageRetransmissions,
|
||||
schema.messageRetransmissions.retryCount,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void markUpdated() {
|
||||
notifyUpdates({TableUpdate.onTable(messages, kind: UpdateKind.update)});
|
||||
notifyUpdates({TableUpdate.onTable(contacts, kind: UpdateKind.update)});
|
||||
}
|
||||
|
||||
Future<void> printTableSizes() async {
|
||||
final result = await customSelect(
|
||||
'SELECT name, SUM(pgsize) as size FROM dbstat GROUP BY name',
|
||||
).get();
|
||||
|
||||
for (final row in result) {
|
||||
final tableName = row.read<String>('name');
|
||||
final tableSize = row.read<String>('size');
|
||||
Log.info('Table: $tableName, Size: $tableSize bytes');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteDataForTwonlySafe() async {
|
||||
await delete(messages).go();
|
||||
await delete(messageRetransmissions).go();
|
||||
await delete(mediaUploads).go();
|
||||
await update(contacts).write(
|
||||
const ContactsCompanion(
|
||||
avatarSvg: Value(null),
|
||||
myAvatarCounter: Value(0),
|
||||
),
|
||||
);
|
||||
await delete(signalContactPreKeys).go();
|
||||
await delete(signalContactSignedPreKeys).go();
|
||||
await (delete(signalPreKeyStores)
|
||||
..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
clock.now().subtract(
|
||||
const Duration(days: 25),
|
||||
),
|
||||
)),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,426 +0,0 @@
|
|||
import 'dart:collection' show HashSet;
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:restart_app/restart_app.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/database/twonly_database_old.dart'
|
||||
show TwonlyDatabaseOld;
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
class DatabaseMigrationView extends StatefulWidget {
|
||||
const DatabaseMigrationView({super.key});
|
||||
|
||||
@override
|
||||
State<DatabaseMigrationView> createState() => _DatabaseMigrationViewState();
|
||||
}
|
||||
|
||||
class _DatabaseMigrationViewState extends State<DatabaseMigrationView> {
|
||||
bool _isMigrating = false;
|
||||
bool _isMigratingFinished = false;
|
||||
int _contactsMigrated = 0;
|
||||
int _storedMediaFiles = 0;
|
||||
|
||||
Future<void> startMigration() async {
|
||||
setState(() {
|
||||
_isMigrating = true;
|
||||
});
|
||||
|
||||
final oldDatabase = TwonlyDatabaseOld();
|
||||
final oldContacts = await oldDatabase.contacts.select().get();
|
||||
|
||||
for (final oldContact in oldContacts) {
|
||||
try {
|
||||
if (oldContact.deleted) continue;
|
||||
Uint8List? avatarSvg;
|
||||
if (oldContact.avatarSvg != null) {
|
||||
avatarSvg = Uint8List.fromList(
|
||||
gzip.encode(utf8.encode(oldContact.avatarSvg!)),
|
||||
);
|
||||
}
|
||||
await twonlyDB.contactsDao.insertContact(
|
||||
ContactsCompanion(
|
||||
userId: Value(oldContact.userId),
|
||||
username: Value(oldContact.username),
|
||||
displayName: Value(oldContact.displayName),
|
||||
nickName: Value(oldContact.nickName),
|
||||
avatarSvgCompressed: Value(avatarSvg),
|
||||
senderProfileCounter: const Value(0),
|
||||
accepted: Value(oldContact.accepted),
|
||||
requested: Value(oldContact.requested),
|
||||
blocked: Value(oldContact.blocked),
|
||||
verified: Value(oldContact.verified),
|
||||
createdAt: Value(oldContact.createdAt),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_contactsMigrated += 1;
|
||||
});
|
||||
await twonlyDB.groupsDao.createNewDirectChat(
|
||||
oldContact.userId,
|
||||
GroupsCompanion(
|
||||
pinned: Value(oldContact.pinned),
|
||||
archived: Value(oldContact.archived),
|
||||
groupName: Value(getContactDisplayNameOld(oldContact)),
|
||||
totalMediaCounter: Value(oldContact.totalMediaCounter),
|
||||
alsoBestFriend: Value(oldContact.alsoBestFriend),
|
||||
createdAt: Value(oldContact.createdAt),
|
||||
lastFlameCounterChange: Value(oldContact.lastFlameCounterChange),
|
||||
lastFlameSync: Value(oldContact.lastFlameSync),
|
||||
lastMessageExchange: Value(oldContact.lastMessageExchange),
|
||||
lastMessageReceived: Value(oldContact.lastMessageReceived),
|
||||
lastMessageSend: Value(oldContact.lastMessageSend),
|
||||
flameCounter: Value(oldContact.flameCounter),
|
||||
maxFlameCounter: Value(oldContact.flameCounter),
|
||||
maxFlameCounterFrom: Value(clock.now()),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
final folders = ['memories', 'send', 'received'];
|
||||
|
||||
final alreadyCopied = HashSet();
|
||||
|
||||
for (final folder in folders) {
|
||||
final memoriesPath = Directory(
|
||||
join(
|
||||
(await getApplicationSupportDirectory()).path,
|
||||
'media',
|
||||
folder,
|
||||
),
|
||||
);
|
||||
if (memoriesPath.existsSync()) {
|
||||
final files = memoriesPath.listSync();
|
||||
for (final file in files) {
|
||||
try {
|
||||
if (file.path.contains('thumbnail')) continue;
|
||||
late MediaType type;
|
||||
if (file.path.contains('mp4')) {
|
||||
type = MediaType.video;
|
||||
} else if (file.path.contains('png')) {
|
||||
type = MediaType.image;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
final bytes = File(file.path).readAsBytesSync();
|
||||
final digest = (await Sha256().hash(bytes)).bytes;
|
||||
if (alreadyCopied.contains(digest)) {
|
||||
continue;
|
||||
}
|
||||
alreadyCopied.add(digest);
|
||||
|
||||
final stat = FileStat.statSync(file.path);
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||
MediaFilesCompanion(
|
||||
type: Value(type),
|
||||
createdAt: Value(stat.modified),
|
||||
stored: const Value(true),
|
||||
),
|
||||
);
|
||||
final mediaService = MediaFileService(mediaFile!);
|
||||
File(file.path).copySync(mediaService.storedPath.path);
|
||||
setState(() {
|
||||
_storedMediaFiles += 1;
|
||||
});
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final oldContactPreKeys =
|
||||
await oldDatabase.signalContactPreKeys.select().get();
|
||||
for (final oldContactPreKey in oldContactPreKeys) {
|
||||
try {
|
||||
await twonlyDB
|
||||
.into(twonlyDB.signalContactPreKeys)
|
||||
.insert(SignalContactPreKey.fromJson(oldContactPreKey.toJson()));
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
final oldSignalSessionStores =
|
||||
await oldDatabase.signalSessionStores.select().get();
|
||||
for (final oldSignalSessionStore in oldSignalSessionStores) {
|
||||
try {
|
||||
await twonlyDB.into(twonlyDB.signalSessionStores).insert(
|
||||
SignalSessionStore.fromJson(oldSignalSessionStore.toJson()),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
final oldSignalSenderKeyStores =
|
||||
await oldDatabase.signalSenderKeyStores.select().get();
|
||||
for (final oldSignalSenderKeyStore in oldSignalSenderKeyStores) {
|
||||
try {
|
||||
await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert(
|
||||
SignalSenderKeyStore.fromJson(oldSignalSenderKeyStore.toJson()),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
final oldSignalPreyKeyStores =
|
||||
await oldDatabase.signalPreKeyStores.select().get();
|
||||
for (final oldSignalPreyKeyStore in oldSignalPreyKeyStores) {
|
||||
try {
|
||||
await twonlyDB
|
||||
.into(twonlyDB.signalPreKeyStores)
|
||||
.insert(SignalPreKeyStore.fromJson(oldSignalPreyKeyStore.toJson()));
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
final oldSignalIdentityKeyStores =
|
||||
await oldDatabase.signalIdentityKeyStores.select().get();
|
||||
for (final oldSignalIdentityKeyStore in oldSignalIdentityKeyStores) {
|
||||
try {
|
||||
await twonlyDB.into(twonlyDB.signalIdentityKeyStores).insert(
|
||||
SignalIdentityKeyStore.fromJson(
|
||||
oldSignalIdentityKeyStore.toJson(),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
final oldSignalContactSignedPreKeys =
|
||||
await oldDatabase.signalContactSignedPreKeys.select().get();
|
||||
for (final oldSignalContactSignedPreKey in oldSignalContactSignedPreKeys) {
|
||||
try {
|
||||
await twonlyDB.into(twonlyDB.signalContactSignedPreKeys).insert(
|
||||
SignalContactSignedPreKey.fromJson(
|
||||
oldSignalContactSignedPreKey.toJson(),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
await updateUserdata((u) {
|
||||
u.appVersion = 62;
|
||||
return u;
|
||||
});
|
||||
|
||||
setState(() {
|
||||
_isMigratingFinished = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: _isMigratingFinished
|
||||
? ListView(
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
const Text(
|
||||
'Deine Daten wurden migriert.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 35,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
...[
|
||||
'$_contactsMigrated Kontakte',
|
||||
'$_storedMediaFiles gespeicherte Mediendateien',
|
||||
].map(
|
||||
(e) => Text(
|
||||
e,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 17),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
const Text(
|
||||
'Sollte du feststellen, dass es bei der Migration Fehler gab, zum Beispiel, dass Bilder fehlen, dann melde dies bitte über das Feedback-Formular. Du hast dafür eine Woche Zeit, danach werden deine alte Daten unwiederruflich gelöscht.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Restart.restartApp(
|
||||
notificationTitle: 'Deine Daten wurden migriert.',
|
||||
notificationBody: 'Click here to open the app again',
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
'App neu starten',
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: _isMigrating
|
||||
? ListView(
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
const Text(
|
||||
'Deine Daten werden migriert.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 35,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
const Center(
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
const Text(
|
||||
'twonly während der Migration NICHT schließen!',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 20, color: Colors.red),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
const Text(
|
||||
'Aktueller Status',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 20),
|
||||
),
|
||||
...[
|
||||
'$_contactsMigrated Kontakte',
|
||||
'$_storedMediaFiles gespeicherte Mediendateien',
|
||||
].map(
|
||||
(e) => Text(
|
||||
e,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 17),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
const Text(
|
||||
'twonly. Besser als je zuvor.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 35,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
const Text(
|
||||
'Das sind die neuen Features.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 20),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
...[
|
||||
'Gruppen',
|
||||
'Nachrichten bearbeiten & löschen',
|
||||
].map(
|
||||
(e) => Text(
|
||||
e,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 17),
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'Technische Neuerungen',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 17),
|
||||
),
|
||||
...[
|
||||
'Client-to-Client (C2C) Protokoll umgestellt auf ProtoBuf.',
|
||||
'Verwendung von UUIDs in der Datenbank',
|
||||
'Von Grund auf neues Datenbank-Schema',
|
||||
'Verbesserung der Zuverlässigkeit von C2C Nachrichten',
|
||||
'Verbesserung von Videos',
|
||||
].map(
|
||||
(e) => Text(
|
||||
e,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 10),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
const Text(
|
||||
'Was bedeutet das für dich?',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 20),
|
||||
),
|
||||
const Text(
|
||||
'Aufgrund der technischen Umstellung müssen wir deine alte Datenbank sowie deine gespeicherten Bilder migieren. Durch die Migration gehen einige Informationen verloren.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Was nach der Migration erhalten bleibt.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 15),
|
||||
),
|
||||
...[
|
||||
'Gespeicherte Bilder',
|
||||
'Kontakte',
|
||||
'Flammen',
|
||||
].map(
|
||||
(e) => Text(
|
||||
e,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Was durch die Migration verloren geht.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 15, color: Colors.red),
|
||||
),
|
||||
...[
|
||||
'Text-Nachrichten und Reaktionen',
|
||||
'Alles, was gesendet wurde, aber noch nicht empfangen wurde, wie Nachrichten und Bilder.',
|
||||
].map(
|
||||
(e) => Text(
|
||||
e,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
FilledButton(
|
||||
onPressed: startMigration,
|
||||
child: const Text(
|
||||
'Jetzt starten',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue