mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18:41 +00:00
start using drift
This commit is contained in:
parent
00ad654660
commit
f7306fe7db
48 changed files with 3138 additions and 807 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:twonly/src/providers/api_provider.dart';
|
||||
import 'package:twonly/src/providers/db_provider.dart';
|
||||
|
||||
late DbProvider dbProvider;
|
||||
late ApiProvider apiProvider;
|
||||
late DbProvider dbProvider;
|
||||
|
|
|
|||
|
|
@ -2,14 +2,12 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
import 'package:twonly/src/providers/api/api.dart';
|
||||
import 'package:twonly/src/providers/api_provider.dart';
|
||||
import 'package:twonly/src/providers/db_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:twonly/src/providers/download_change_provider.dart';
|
||||
import 'package:twonly/src/providers/messages_change_provider.dart';
|
||||
import 'package:twonly/src/providers/contacts_change_provider.dart';
|
||||
import 'package:twonly/src/providers/db_provider.dart';
|
||||
import 'package:twonly/src/providers/send_next_media_to.dart';
|
||||
import 'package:twonly/src/providers/settings_change_provider.dart';
|
||||
import 'package:twonly/src/services/fcm_service.dart';
|
||||
|
|
@ -49,9 +47,10 @@ void main() async {
|
|||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => MessagesChangeProvider()),
|
||||
ChangeNotifierProvider(create: (_) => DownloadChangeProvider()),
|
||||
ChangeNotifierProvider(create: (_) => ContactChangeProvider()),
|
||||
Provider<TwonlyDatabase>(
|
||||
create: (context) => TwonlyDatabase(),
|
||||
dispose: (context, db) => db.close(),
|
||||
),
|
||||
ChangeNotifierProvider(create: (_) => SendNextMediaTo()),
|
||||
ChangeNotifierProvider(create: (_) => settingsController),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/components/connection_state.dart';
|
||||
import 'package:twonly/src/providers/contacts_change_provider.dart';
|
||||
import 'package:twonly/src/providers/download_change_provider.dart';
|
||||
import 'package:twonly/src/providers/messages_change_provider.dart';
|
||||
import 'package:twonly/src/providers/settings_change_provider.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/onboarding/onboarding_view.dart';
|
||||
|
|
@ -23,9 +20,6 @@ Function(bool) globalCallbackConnectionState = (a) {};
|
|||
bool globalIsAppInBackground = true;
|
||||
|
||||
// these two callbacks are called on updated to the corresponding database
|
||||
Function globalCallBackOnContactChange = () {};
|
||||
Future Function(int, int?) globalCallBackOnMessageChange = (a, b) async {};
|
||||
Function(List<int>, bool) globalCallBackOnDownloadChange = (a, b) {};
|
||||
|
||||
/// The Widget that configures your application.
|
||||
class MyApp extends StatefulWidget {
|
||||
|
|
@ -45,10 +39,6 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||
globalIsAppInBackground = false;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
// init change provider to load data from the database
|
||||
context.read<ContactChangeProvider>().update();
|
||||
context.read<MessagesChangeProvider>().init();
|
||||
|
||||
// register global callbacks to the widget tree
|
||||
globalCallbackConnectionState = (isConnected) {
|
||||
setState(() {
|
||||
|
|
@ -56,20 +46,6 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||
});
|
||||
};
|
||||
|
||||
globalCallBackOnContactChange = () {
|
||||
context.read<ContactChangeProvider>().update();
|
||||
};
|
||||
|
||||
globalCallBackOnDownloadChange = (token, add) {
|
||||
context.read<DownloadChangeProvider>().update(token, add);
|
||||
};
|
||||
|
||||
globalCallBackOnMessageChange = (userId, messageId) async {
|
||||
await context
|
||||
.read<MessagesChangeProvider>()
|
||||
.updateLastMessageFor(userId, messageId);
|
||||
};
|
||||
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// _requestPermissions();
|
||||
// _initService();
|
||||
|
|
@ -124,8 +100,6 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||
if (wasPaused) {
|
||||
globalIsAppInBackground = false;
|
||||
apiProvider.connect();
|
||||
context.read<ContactChangeProvider>().update();
|
||||
context.read<MessagesChangeProvider>().init();
|
||||
// _stopService();
|
||||
}
|
||||
} else if (state == AppLifecycleState.paused) {
|
||||
|
|
@ -145,9 +119,6 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||
WidgetsBinding.instance.removeObserver(this);
|
||||
// disable globalCallbacks to the flutter tree
|
||||
globalCallbackConnectionState = (a) {};
|
||||
globalCallBackOnDownloadChange = (a, b) {};
|
||||
globalCallBackOnContactChange = () {};
|
||||
globalCallBackOnMessageChange = (a, b) async {};
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
import 'dart:collection';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/src/components/verified_shield.dart';
|
||||
import 'package:twonly/src/providers/messages_change_provider.dart';
|
||||
import 'package:twonly/src/database/contacts_db.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/components/flame.dart';
|
||||
import 'package:twonly/src/components/headline.dart';
|
||||
import 'package:twonly/src/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
|
||||
class BestFriendsSelector extends StatelessWidget {
|
||||
final List<Contact> users;
|
||||
final Function(Int64, bool) updateStatus;
|
||||
final HashSet<Int64> selectedUserIds;
|
||||
final Function(int, bool) updateStatus;
|
||||
final HashSet<int> selectedUserIds;
|
||||
final int maxTotalMediaCounter;
|
||||
final bool isRealTwonly;
|
||||
|
||||
|
|
@ -80,7 +78,7 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
|
||||
class UserCheckbox extends StatelessWidget {
|
||||
final Contact user;
|
||||
final Function(Int64, bool) onChanged;
|
||||
final Function(int, bool) onChanged;
|
||||
final bool isChecked;
|
||||
final bool isRealTwonly;
|
||||
final int maxTotalMediaCounter;
|
||||
|
|
@ -96,10 +94,7 @@ class UserCheckbox extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int flameCounter = context
|
||||
.watch<MessagesChangeProvider>()
|
||||
.flamesCounter[user.userId.toInt()] ??
|
||||
0;
|
||||
String displayName = getContactDisplayName(user);
|
||||
|
||||
return Container(
|
||||
padding:
|
||||
|
|
@ -120,8 +115,8 @@ class UserCheckbox extends StatelessWidget {
|
|||
child: Row(
|
||||
children: [
|
||||
InitialsAvatar(
|
||||
displayName,
|
||||
fontSize: 12,
|
||||
displayName: user.displayName,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Column(
|
||||
|
|
@ -138,16 +133,23 @@ class UserCheckbox extends StatelessWidget {
|
|||
size: 12,
|
||||
)),
|
||||
Text(
|
||||
user.displayName.length > 10
|
||||
? '${user.displayName.substring(0, 10)}...'
|
||||
: user.displayName,
|
||||
displayName.length > 10
|
||||
? '${displayName.substring(0, 10)}...'
|
||||
: displayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (flameCounter > 0)
|
||||
FlameCounterWidget(
|
||||
user, flameCounter, maxTotalMediaCounter),
|
||||
StreamBuilder(
|
||||
stream: context.db.watchFlameCounter(user.userId),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData && snapshot.data! != 0) {
|
||||
return Container();
|
||||
}
|
||||
return FlameCounterWidget(
|
||||
user, snapshot.data!, maxTotalMediaCounter);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/components/animate_icon.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
|
||||
class FlameCounterWidget extends StatelessWidget {
|
||||
final Contact user;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ class InitialsAvatar extends StatelessWidget {
|
|||
final String displayName;
|
||||
final double? fontSize;
|
||||
|
||||
const InitialsAvatar(
|
||||
{super.key, required this.displayName, this.fontSize = 20});
|
||||
const InitialsAvatar(this.displayName, {super.key, this.fontSize = 20});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
import 'package:twonly/src/database/messages_db.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/messages_model.dart';
|
||||
import 'package:twonly/src/providers/download_change_provider.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
enum MessageSendState {
|
||||
|
|
@ -15,21 +15,75 @@ enum MessageSendState {
|
|||
sending,
|
||||
}
|
||||
|
||||
class MessageSendStateIcon extends StatelessWidget {
|
||||
final DbMessage message;
|
||||
MessageSendState messageSendStateFromMessage(Message msg) {
|
||||
MessageSendState state;
|
||||
|
||||
if (!msg.acknowledgeByServer) {
|
||||
state = MessageSendState.sending;
|
||||
} else {
|
||||
if (msg.messageOtherId == null) {
|
||||
// message send
|
||||
if (msg.openedAt == null) {
|
||||
state = MessageSendState.send;
|
||||
} else {
|
||||
state = MessageSendState.sendOpened;
|
||||
}
|
||||
} else {
|
||||
// message received
|
||||
if (msg.openedAt == null) {
|
||||
state = MessageSendState.received;
|
||||
} else {
|
||||
state = MessageSendState.receivedOpened;
|
||||
}
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
class MessageSendStateIcon extends StatefulWidget {
|
||||
final List<Message> messages;
|
||||
final MainAxisAlignment mainAxisAlignment;
|
||||
|
||||
const MessageSendStateIcon(this.message,
|
||||
const MessageSendStateIcon(this.messages,
|
||||
{super.key, this.mainAxisAlignment = MainAxisAlignment.end});
|
||||
|
||||
@override
|
||||
State<MessageSendStateIcon> createState() => _MessageSendStateIconState();
|
||||
}
|
||||
|
||||
class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||
bool containsVideo = false;
|
||||
bool containsText = false;
|
||||
bool containsImage = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
for (Message msg in widget.messages) {
|
||||
if (msg.kind == MessageKind.textMessage) {
|
||||
containsText = true;
|
||||
}
|
||||
if (msg.kind == MessageKind.media) {
|
||||
MessageJson message =
|
||||
MessageJson.fromJson(jsonDecode(msg.contentJson!));
|
||||
final content = message.content;
|
||||
if (content is MediaMessageContent) {
|
||||
if (content.isVideo) {
|
||||
containsVideo = true;
|
||||
} else {
|
||||
containsImage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget icon = Placeholder();
|
||||
String text = "";
|
||||
|
||||
Color color =
|
||||
message.messageContent.getColor(Theme.of(context).colorScheme.primary);
|
||||
|
||||
Widget loaderIcon = Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
|
|
@ -41,7 +95,9 @@ class MessageSendStateIcon extends StatelessWidget {
|
|||
],
|
||||
);
|
||||
|
||||
switch (message.getSendState()) {
|
||||
MessageSendState state = messageSendStateFromMessage(message);
|
||||
|
||||
switch (state) {
|
||||
case MessageSendState.receivedOpened:
|
||||
icon = Icon(Icons.crop_square, size: 14, color: color);
|
||||
text = context.lang.messageSendState_Received;
|
||||
|
|
@ -65,24 +121,16 @@ class MessageSendStateIcon extends StatelessWidget {
|
|||
break;
|
||||
}
|
||||
|
||||
if (!message.isDownloaded) {
|
||||
if (message.downloadState == DownloadState.pending) {
|
||||
text = context.lang.messageSendState_TapToLoad;
|
||||
}
|
||||
|
||||
bool isDownloading = false;
|
||||
final content = message.messageContent;
|
||||
if (message.messageReceived && content is MediaMessageContent) {
|
||||
final test = context.watch<DownloadChangeProvider>().currentlyDownloading;
|
||||
isDownloading = test.contains(content.downloadToken.toString());
|
||||
}
|
||||
|
||||
if (isDownloading) {
|
||||
if (message.downloadState == DownloadState.downloaded) {
|
||||
text = context.lang.messageSendState_Loading;
|
||||
icon = loaderIcon;
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: mainAxisAlignment,
|
||||
mainAxisAlignment: widget.mainAxisAlignment,
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 3),
|
||||
|
|
|
|||
|
|
@ -2,17 +2,19 @@ import 'package:flutter/material.dart';
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
import 'package:twonly/src/providers/send_next_media_to.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/chats/chat_item_details_view.dart';
|
||||
import 'package:twonly/src/views/contact/contact_verify_view.dart';
|
||||
import 'package:twonly/src/views/home_view.dart';
|
||||
|
||||
class UserContextMenu extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Contact user;
|
||||
final Contact contact;
|
||||
|
||||
const UserContextMenu({super.key, required this.user, required this.child});
|
||||
const UserContextMenu(
|
||||
{super.key, required this.contact, required this.child});
|
||||
|
||||
@override
|
||||
State<UserContextMenu> createState() => _UserContextMenuState();
|
||||
|
|
@ -22,38 +24,38 @@ class _UserContextMenuState extends State<UserContextMenu> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PieMenu(
|
||||
onPressed: () => print('pressed'),
|
||||
onPressed: () => (),
|
||||
actions: [
|
||||
PieAction(
|
||||
tooltip: const Text('Verify user'),
|
||||
tooltip: Text(context.lang.contextMenuVerifyUser),
|
||||
onSelect: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ContactVerifyView(widget.user);
|
||||
return ContactVerifyView(widget.contact);
|
||||
},
|
||||
));
|
||||
},
|
||||
child: widget.user.verified
|
||||
child: widget.contact.verified
|
||||
? FaIcon(FontAwesomeIcons.shieldHeart)
|
||||
: const Icon(Icons.gpp_maybe_rounded), // Can be any widget
|
||||
: const Icon(Icons.gpp_maybe_rounded),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: const Text('Open chat'),
|
||||
tooltip: Text(context.lang.contextMenuOpenChat),
|
||||
onSelect: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ChatItemDetailsView(user: widget.user);
|
||||
return ChatItemDetailsView(user: widget.contact);
|
||||
},
|
||||
));
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.solidComments),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: const Text('Send image'),
|
||||
tooltip: Text(context.lang.contextMenuSendImage),
|
||||
onSelect: () {
|
||||
context
|
||||
.read<SendNextMediaTo>()
|
||||
.updateSendNextMediaTo(widget.user.userId.toInt());
|
||||
.updateSendNextMediaTo(widget.contact.userId.toInt());
|
||||
globalUpdateOfHomeViewPageIndex(0);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.camera),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
|
||||
class VerifiedShield extends StatelessWidget {
|
||||
final Contact contact;
|
||||
|
|
|
|||
53
lib/src/database/contacts_db.dart
Normal file
53
lib/src/database/contacts_db.dart
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
|
||||
class Contacts extends Table {
|
||||
IntColumn get userId => integer()();
|
||||
|
||||
TextColumn get username => text().unique()();
|
||||
TextColumn get displayName => text().nullable()();
|
||||
TextColumn get nickName => text().nullable()();
|
||||
|
||||
BoolColumn get accepted => boolean().withDefault(Constant(false))();
|
||||
BoolColumn get requested => boolean().withDefault(Constant(false))();
|
||||
BoolColumn get blocked => boolean().withDefault(Constant(false))();
|
||||
BoolColumn get verified => boolean().withDefault(Constant(false))();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
IntColumn get totalMediaCounter => integer().withDefault(Constant(0))();
|
||||
|
||||
DateTimeColumn get lastMessageSend => dateTime().nullable()();
|
||||
DateTimeColumn get lastMessageReceived => dateTime().nullable()();
|
||||
DateTimeColumn get lastMessage => dateTime().nullable()();
|
||||
|
||||
IntColumn get flameCounter => integer().withDefault(Constant(0))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {userId};
|
||||
}
|
||||
|
||||
String getContactDisplayName(Contact user) {
|
||||
if (user.nickName != null) {
|
||||
return user.nickName!;
|
||||
}
|
||||
if (user.displayName != null) {
|
||||
return user.displayName!;
|
||||
}
|
||||
return user.username;
|
||||
}
|
||||
|
||||
int getFlameCounterFromContact(Contact contact) {
|
||||
if (contact.lastMessageSend == null || contact.lastMessageReceived == null) {
|
||||
return 0;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||
final twoDaysAgo = startOfToday.subtract(Duration(days: 2));
|
||||
if (contact.lastMessageSend!.isBefore(twoDaysAgo) &&
|
||||
contact.lastMessageReceived!.isBefore(twoDaysAgo)) {
|
||||
return contact.flameCounter;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
113
lib/src/database/database.dart
Normal file
113
lib/src/database/database.dart
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/database/contacts_db.dart';
|
||||
import 'package:twonly/src/database/messages_db.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
// You can then create a database class that includes this table
|
||||
@DriftDatabase(tables: [Contacts, Messages])
|
||||
class TwonlyDatabase extends _$TwonlyDatabase {
|
||||
TwonlyDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
name: 'twonly_main_db',
|
||||
native: const DriftNativeOptions(
|
||||
databaseDirectory: getApplicationSupportDirectory,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------
|
||||
|
||||
Stream<List<Message>> watchMessageNotOpened(int userId) {
|
||||
return (select(messages)
|
||||
..where((t) => t.openedAt.isNull() & t.contactId.equals(userId)))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<Message?> watchLastMessage(int userId) {
|
||||
return (select(messages)
|
||||
..where((t) => t.contactId.equals(userId))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.sendAt)])
|
||||
..limit(1))
|
||||
.watchSingleOrNull();
|
||||
}
|
||||
|
||||
// ------------
|
||||
|
||||
Future<int> insertContact(ContactsCompanion contact) {
|
||||
return into(contacts).insert(contact);
|
||||
}
|
||||
|
||||
SingleOrNullSelectable<Contact> getContactByUserId(int userId) {
|
||||
return select(contacts)..where((t) => t.userId.equals(userId));
|
||||
}
|
||||
|
||||
// Stream<int> getMaxTotalMediaCounter() {
|
||||
// return customSelect(
|
||||
// 'SELECT MAX(totalMediaCounter) AS maxTotal FROM contacts',
|
||||
// readsFrom: {contacts},
|
||||
// ).watchSingle().asyncMap((result) {
|
||||
// return result.read<int>('maxTotal');
|
||||
// });
|
||||
// }
|
||||
|
||||
Future deleteContactByUserId(int userId) {
|
||||
return (delete(contacts)..where((t) => t.userId.equals(userId))).go();
|
||||
}
|
||||
|
||||
Future updateContact(int userId, ContactsCompanion updatedValues) {
|
||||
return (update(contacts)..where((c) => c.userId.equals(userId)))
|
||||
.write(updatedValues);
|
||||
}
|
||||
|
||||
Stream<List<Contact>> watchNotAcceptedContacts() {
|
||||
return (select(contacts)..where((t) => t.accepted.equals(false))).watch();
|
||||
}
|
||||
|
||||
Stream<List<Contact>> watchContactsForChatList() {
|
||||
return (select(contacts)
|
||||
..where((t) => t.accepted.equals(true) & t.blocked.equals(false))
|
||||
..orderBy([(t) => OrderingTerm.asc(t.lastMessage)]))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<int?> watchContactsBlocked() {
|
||||
final count = contacts.blocked.count(distinct: true);
|
||||
final query = selectOnly(contacts)..where(contacts.blocked.equals(true));
|
||||
query.addColumns([count]);
|
||||
return query.map((row) => row.read(count)).watchSingle();
|
||||
}
|
||||
|
||||
Stream<int?> watchContactsRequested() {
|
||||
final count = contacts.requested.count(distinct: true);
|
||||
final query = selectOnly(contacts)..where(contacts.requested.equals(true));
|
||||
query.addColumns([count]);
|
||||
return query.map((row) => row.read(count)).watchSingle();
|
||||
}
|
||||
|
||||
Stream<List<Contact>> watchAllContacts() {
|
||||
return select(contacts).watch();
|
||||
}
|
||||
|
||||
Stream<int> watchFlameCounter(int userId) {
|
||||
return (select(contacts)
|
||||
..where(
|
||||
(u) =>
|
||||
u.userId.equals(userId) &
|
||||
u.lastMessageReceived.isNotNull() &
|
||||
u.lastMessageSend.isNotNull(),
|
||||
))
|
||||
.watchSingle()
|
||||
.asyncMap((contact) {
|
||||
return getFlameCounterFromContact(contact);
|
||||
});
|
||||
}
|
||||
}
|
||||
2186
lib/src/database/database.g.dart
Normal file
2186
lib/src/database/database.g.dart
Normal file
File diff suppressed because it is too large
Load diff
32
lib/src/database/messages_db.dart
Normal file
32
lib/src/database/messages_db.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/src/database/contacts_db.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
|
||||
enum DownloadState {
|
||||
pending,
|
||||
downloading,
|
||||
downloaded,
|
||||
}
|
||||
|
||||
class Messages extends Table {
|
||||
IntColumn get contactId => integer().references(Contacts, #userId)();
|
||||
|
||||
IntColumn get messageId => integer().autoIncrement()();
|
||||
IntColumn get messageOtherId => integer().nullable()();
|
||||
|
||||
IntColumn get responseToMessageId => integer().nullable()();
|
||||
IntColumn get responseToOtherMessageId => integer().nullable()();
|
||||
|
||||
BoolColumn get acknowledgeByUser => boolean().withDefault(Constant(false))();
|
||||
IntColumn get downloadState => intEnum<DownloadState>()();
|
||||
|
||||
BoolColumn get acknowledgeByServer =>
|
||||
boolean().withDefault(Constant(false))();
|
||||
|
||||
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)();
|
||||
}
|
||||
|
|
@ -40,6 +40,9 @@
|
|||
"chatListViewSearchUserNameBtn": "Füge deinen ersten twonly-Kontakt hinzu!",
|
||||
"chatListViewSendFirstTwonly": "Sende dein erstes twonly!",
|
||||
"chatListDetailInput": "Nachricht eingeben",
|
||||
"contextMenuVerifyUser": "Kontakt verifizieren",
|
||||
"contextMenuOpenChat": "Chat öffnen",
|
||||
"contextMenuSendImage": "Bild senden",
|
||||
"messageSendState_Received": "Empfangen",
|
||||
"messageSendState_Opened": "Geöffnet",
|
||||
"messageSendState_Send": "Gesendet",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@
|
|||
"chatListViewSearchUserNameBtn": "Add your first twonly contact!",
|
||||
"chatListViewSendFirstTwonly": "Send your first twonly!",
|
||||
"chatListDetailInput": "Type a message",
|
||||
"contextMenuVerifyUser": "Verify user",
|
||||
"contextMenuOpenChat": "Open chat",
|
||||
"contextMenuSendImage": "Send image",
|
||||
"mediaViewerAuthReason": "Please authenticate to see this twonly!",
|
||||
"messageSendState_Received": "Received",
|
||||
"messageSendState_Opened": "Opened",
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
enum MessageKind {
|
||||
textMessage,
|
||||
image,
|
||||
video,
|
||||
media,
|
||||
contactRequest,
|
||||
rejectRequest,
|
||||
acceptRequest,
|
||||
|
|
@ -11,63 +10,13 @@ enum MessageKind {
|
|||
ack
|
||||
}
|
||||
|
||||
extension MessageKindExtension on MessageKind {
|
||||
String get name => toString().split('.').last;
|
||||
|
||||
static MessageKind fromString(String name) {
|
||||
return MessageKind.values.firstWhere((e) => e.name == name);
|
||||
}
|
||||
|
||||
int get index => this.index;
|
||||
|
||||
static MessageKind fromIndex(int index) {
|
||||
return MessageKind.values[index];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use message as base class, remove kind and flatten content
|
||||
class Message {
|
||||
final MessageKind kind;
|
||||
final MessageContent content;
|
||||
final int? messageId;
|
||||
DateTime timestamp;
|
||||
|
||||
Message(
|
||||
{required this.kind,
|
||||
this.messageId,
|
||||
required this.content,
|
||||
required this.timestamp});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Message(kind: $kind, content: $content, timestamp: $timestamp)';
|
||||
}
|
||||
|
||||
static Message fromJson(Map<String, dynamic> json) => Message(
|
||||
kind: MessageKindExtension.fromString(json["kind"]),
|
||||
messageId: (json['messageId'] as num?)?.toInt(),
|
||||
content:
|
||||
MessageContent.fromJson(json['content'] as Map<String, dynamic>),
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||
'kind': kind.name,
|
||||
'content': content.toJson(),
|
||||
'messageId': messageId,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
class MessageContent {
|
||||
MessageContent();
|
||||
|
||||
Color getColor(Color primary) {
|
||||
Color getMessageColorFromType(MessageJson msg, Color primary) {
|
||||
Color color;
|
||||
if (this is TextMessageContent) {
|
||||
|
||||
final content = msg.content;
|
||||
if (content is TextMessageContent) {
|
||||
color = Colors.lightBlue;
|
||||
} else {
|
||||
final content = this;
|
||||
if (content is MediaMessageContent) {
|
||||
if (content.isRealTwonly) {
|
||||
color = primary;
|
||||
|
|
@ -83,16 +32,64 @@ class MessageContent {
|
|||
}
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
extension MessageKindExtension on MessageKind {
|
||||
String get name => toString().split('.').last;
|
||||
|
||||
static MessageKind fromString(String name) {
|
||||
return MessageKind.values.firstWhere((e) => e.name == name);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageJson {
|
||||
final MessageKind kind;
|
||||
final MessageContent? content;
|
||||
final int? messageId;
|
||||
DateTime timestamp;
|
||||
|
||||
MessageJson(
|
||||
{required this.kind,
|
||||
this.messageId,
|
||||
required this.content,
|
||||
required this.timestamp});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Message(kind: $kind, content: $content, timestamp: $timestamp)';
|
||||
}
|
||||
|
||||
static MessageContent fromJson(Map json) {
|
||||
switch (json['type']) {
|
||||
case 'MediaMessageContent':
|
||||
static MessageJson fromJson(Map<String, dynamic> json) {
|
||||
final kind = MessageKindExtension.fromString(json["kind"]);
|
||||
|
||||
return MessageJson(
|
||||
kind: kind,
|
||||
messageId: (json['messageId'] as num?)?.toInt(),
|
||||
content: MessageContent.fromJson(
|
||||
kind, json['content'] as Map<String, dynamic>),
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||
'kind': kind.name,
|
||||
'content': content?.toJson(),
|
||||
'messageId': messageId,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
class MessageContent {
|
||||
MessageContent();
|
||||
|
||||
static MessageContent? fromJson(MessageKind kind, Map json) {
|
||||
switch (kind) {
|
||||
case MessageKind.media:
|
||||
return MediaMessageContent.fromJson(json);
|
||||
case 'TextMessageContent':
|
||||
case MessageKind.textMessage:
|
||||
return TextMessageContent.fromJson(json);
|
||||
default:
|
||||
return MessageContent();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +122,6 @@ class MediaMessageContent extends MessageContent {
|
|||
@override
|
||||
Map toJson() {
|
||||
return {
|
||||
'type': 'MediaMessageContent',
|
||||
'downloadToken': downloadToken,
|
||||
'isRealTwonly': isRealTwonly,
|
||||
'maxShowTime': maxShowTime,
|
||||
|
|
@ -146,7 +142,6 @@ class TextMessageContent extends MessageContent {
|
|||
@override
|
||||
Map toJson() {
|
||||
return {
|
||||
'type': 'TextMessageContent',
|
||||
'text': text,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:twonly/src/utils/json.dart';
|
||||
part 'signal_identity.g.dart';
|
||||
|
|
@ -9,8 +8,7 @@ class SignalIdentity {
|
|||
const SignalIdentity(
|
||||
{required this.identityKeyPairU8List, required this.registrationId});
|
||||
|
||||
@Int64Converter()
|
||||
final Int64 registrationId;
|
||||
final int registrationId;
|
||||
|
||||
@Uint8ListConverter()
|
||||
final Uint8List identityKeyPairU8List;
|
||||
|
|
|
|||
|
|
@ -10,13 +10,12 @@ SignalIdentity _$SignalIdentityFromJson(Map<String, dynamic> json) =>
|
|||
SignalIdentity(
|
||||
identityKeyPairU8List: const Uint8ListConverter()
|
||||
.fromJson(json['identityKeyPairU8List'] as String),
|
||||
registrationId:
|
||||
const Int64Converter().fromJson(json['registrationId'] as String),
|
||||
registrationId: (json['registrationId'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SignalIdentityToJson(SignalIdentity instance) =>
|
||||
<String, dynamic>{
|
||||
'registrationId': const Int64Converter().toJson(instance.registrationId),
|
||||
'registrationId': instance.registrationId,
|
||||
'identityKeyPairU8List':
|
||||
const Uint8ListConverter().toJson(instance.identityKeyPairU8List),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:twonly/src/utils/json.dart';
|
||||
part 'user_data.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
|
|
@ -12,8 +10,7 @@ class UserData {
|
|||
final String username;
|
||||
final String displayName;
|
||||
|
||||
@Int64Converter()
|
||||
final Int64 userId;
|
||||
final int userId;
|
||||
|
||||
factory UserData.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserDataFromJson(json);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ part of 'user_data.dart';
|
|||
// **************************************************************************
|
||||
|
||||
UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
||||
userId: const Int64Converter().fromJson(json['userId'] as String),
|
||||
userId: (json['userId'] as num).toInt(),
|
||||
username: json['username'] as String,
|
||||
displayName: json['displayName'] as String,
|
||||
);
|
||||
|
|
@ -15,5 +15,5 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||
'username': instance.username,
|
||||
'displayName': instance.displayName,
|
||||
'userId': const Int64Converter().toJson(instance.userId),
|
||||
'userId': instance.userId,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import 'package:logging/logging.dart';
|
|||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/app.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import '../../../../.blocked/archives/contacts_model.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/messages_model.dart';
|
||||
import '../../../../.blocked/archives/messages_model.dart';
|
||||
import 'package:twonly/src/proto/api/error.pb.dart';
|
||||
import 'package:twonly/src/providers/api/api_utils.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -30,7 +30,7 @@ Future tryTransmitMessages() async {
|
|||
Uint8List? bytes = box.get("retransmit-$msgId-textmessage");
|
||||
if (bytes != null) {
|
||||
Result resp = await apiProvider.sendTextMessage(
|
||||
Int64(retransmit[i].otherUserId),
|
||||
retransmit[i].otherUserId,
|
||||
bytes,
|
||||
);
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ Future tryTransmitMessages() async {
|
|||
if (encryptedMedia != null) {
|
||||
final content = retransmit[i].messageContent;
|
||||
if (content is MediaMessageContent) {
|
||||
uploadMediaFile(msgId, Int64(retransmit[i].otherUserId), encryptedMedia,
|
||||
uploadMediaFile(msgId, retransmit[i].otherUserId, encryptedMedia,
|
||||
content.isRealTwonly, content.maxShowTime, retransmit[i].sendAt);
|
||||
}
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ Future tryTransmitMessages() async {
|
|||
}
|
||||
|
||||
// this functions ensures that the message is received by the server and in case of errors will try again later
|
||||
Future<Result> encryptAndSendMessage(Int64 userId, Message msg) async {
|
||||
Future<Result> encryptAndSendMessage(int userId, Message msg) async {
|
||||
Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId);
|
||||
|
||||
if (bytes == null) {
|
||||
|
|
@ -79,7 +79,7 @@ Future<Result> encryptAndSendMessage(Int64 userId, Message msg) async {
|
|||
return resp;
|
||||
}
|
||||
|
||||
Future sendTextMessage(Int64 target, String message) async {
|
||||
Future sendTextMessage(int target, String message) async {
|
||||
MessageContent content = TextMessageContent(text: message);
|
||||
|
||||
DateTime messageSendAt = DateTime.now();
|
||||
|
|
@ -105,7 +105,7 @@ Future sendTextMessage(Int64 target, String message) async {
|
|||
// this will send the media file and ensures retransmission when errors occur
|
||||
Future uploadMediaFile(
|
||||
int messageId,
|
||||
Int64 target,
|
||||
int target,
|
||||
Uint8List encryptedMedia,
|
||||
bool isRealTwonly,
|
||||
int maxShowTime,
|
||||
|
|
@ -179,7 +179,7 @@ Future uploadMediaFile(
|
|||
}
|
||||
|
||||
class SendImage {
|
||||
final Int64 userId;
|
||||
final int userId;
|
||||
final Uint8List imageBytes;
|
||||
final bool isRealTwonly;
|
||||
final int maxShowTime;
|
||||
|
|
@ -231,7 +231,7 @@ class SendImage {
|
|||
}
|
||||
|
||||
Future sendImage(
|
||||
List<Int64> userIds,
|
||||
List<int> userIds,
|
||||
Uint8List imageBytes,
|
||||
bool isRealTwonly,
|
||||
int maxShowTime,
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/app.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import '../../../../.blocked/archives/contacts_model.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/messages_model.dart';
|
||||
import '../../../../.blocked/archives/messages_model.dart';
|
||||
import 'package:twonly/src/proto/api/client_to_server.pb.dart' as client;
|
||||
import 'package:twonly/src/proto/api/client_to_server.pbserver.dart';
|
||||
import 'package:twonly/src/proto/api/error.pb.dart';
|
||||
|
|
@ -102,7 +102,7 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
|
|||
int? fromUserId = box.get("${data.uploadToken}_fromUserId");
|
||||
if (fromUserId != null) {
|
||||
Uint8List? rawBytes =
|
||||
await SignalHelper.decryptBytes(downloadedBytes, Int64(fromUserId));
|
||||
await SignalHelper.decryptBytes(downloadedBytes, fromUserId);
|
||||
|
||||
if (rawBytes != null) {
|
||||
box.put("${data.uploadToken}_downloaded", rawBytes);
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ class ApiProvider {
|
|||
|
||||
var open = Handshake_OpenSession()
|
||||
..response = signature
|
||||
..userId = userData.userId;
|
||||
..userId = Int64(userData.userId);
|
||||
|
||||
var opensession = Handshake()..opensession = open;
|
||||
|
||||
|
|
@ -304,8 +304,8 @@ class ApiProvider {
|
|||
return await _sendRequestV0(req);
|
||||
}
|
||||
|
||||
Future<Result> getUsername(Int64 userId) async {
|
||||
var get = ApplicationData_GetUserById()..userId = userId;
|
||||
Future<Result> getUsername(int userId) async {
|
||||
var get = ApplicationData_GetUserById()..userId = Int64(userId);
|
||||
var appData = ApplicationData()..getuserbyid = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await _sendRequestV0(req);
|
||||
|
|
@ -353,9 +353,9 @@ class ApiProvider {
|
|||
return await _sendRequestV0(req);
|
||||
}
|
||||
|
||||
Future<Result> sendTextMessage(Int64 target, Uint8List msg) async {
|
||||
Future<Result> sendTextMessage(int target, Uint8List msg) async {
|
||||
var testMessage = ApplicationData_TextMessage()
|
||||
..userId = target
|
||||
..userId = Int64(target)
|
||||
..body = msg;
|
||||
|
||||
var appData = ApplicationData()..textmessage = testMessage;
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/model/messages_model.dart';
|
||||
|
||||
// This provider will update the UI on changes in the contact list
|
||||
class ContactChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||
List<Contact> _allContacts = [];
|
||||
final Map<int, DbMessage> _lastMessagesGroupedByUser = <int, DbMessage>{};
|
||||
|
||||
int get newContactRequests => _allContacts
|
||||
.where((contact) => !contact.accepted && contact.requested)
|
||||
.length;
|
||||
List<Contact> get allContacts => _allContacts;
|
||||
|
||||
void update() async {
|
||||
_allContacts = await DbContacts.getUsers();
|
||||
for (Contact contact in _allContacts) {
|
||||
DbMessage? last = await DbMessages.getLastMessagesForPreviewForUser(
|
||||
contact.userId.toInt());
|
||||
if (last != null) {
|
||||
_lastMessagesGroupedByUser[last.otherUserId] = last;
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Makes `Counter` readable inside the devtools by listing all of its properties
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(IntProperty('count', newContactRequests));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/model/identity_key_store_model.dart';
|
||||
import 'package:twonly/src/model/messages_model.dart';
|
||||
import 'package:twonly/src/model/pre_key_model.dart';
|
||||
import 'package:twonly/src/model/sender_key_store_model.dart';
|
||||
import 'package:twonly/src/model/session_store_model.dart';
|
||||
|
|
@ -55,8 +53,6 @@ class DbProvider {
|
|||
await DbSignalPreKeyStore.setupDatabaseTable(db);
|
||||
await DbSignalSenderKeyStore.setupDatabaseTable(db);
|
||||
await DbSignalIdentityKeyStore.setupDatabaseTable(db);
|
||||
await DbContacts.setupDatabaseTable(db);
|
||||
await DbMessages.setupDatabaseTable(db);
|
||||
}
|
||||
|
||||
Future open() async {
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
import 'dart:collection';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class DownloadChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||
final HashSet<String> _currentlyDownloading = HashSet<String>();
|
||||
|
||||
HashSet<String> get currentlyDownloading => _currentlyDownloading;
|
||||
|
||||
void update(List<int> token, bool add) {
|
||||
debugPrint("Downloading: $add : $token");
|
||||
|
||||
if (add) {
|
||||
_currentlyDownloading.add(token.toString());
|
||||
} else {
|
||||
_currentlyDownloading.remove(token.toString());
|
||||
}
|
||||
debugPrint("Downloading: $add : ${_currentlyDownloading.toList()}");
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/model/messages_model.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
/// This provider does always contains the latest messages send or received
|
||||
/// for every contact.
|
||||
class MessagesChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||
final Map<int, DbMessage> _lastMessage = <int, DbMessage>{};
|
||||
final Map<int, List<DbMessage>> _allMessagesFromUser =
|
||||
<int, List<DbMessage>>{};
|
||||
final Map<int, int> _changeCounter = <int, int>{};
|
||||
final Map<int, int> _flamesCounter = <int, int>{};
|
||||
|
||||
Map<int, DbMessage> get lastMessage => _lastMessage;
|
||||
Map<int, List<DbMessage>> get allMessagesFromUser => _allMessagesFromUser;
|
||||
Map<int, int> get changeCounter => _changeCounter;
|
||||
Map<int, int> get flamesCounter => _flamesCounter;
|
||||
|
||||
Future updateLastMessageFor(int targetUserId, int? messageId) async {
|
||||
DbMessage? last =
|
||||
await DbMessages.getLastMessagesForPreviewForUser(targetUserId);
|
||||
if (last != null) {
|
||||
_lastMessage[last.otherUserId] = last;
|
||||
}
|
||||
flamesCounter[targetUserId] = await getFlamesForOtherUser(targetUserId);
|
||||
// notifyListeners();
|
||||
|
||||
if (messageId == null || _allMessagesFromUser[targetUserId] == null) {
|
||||
loadMessagesForUser(targetUserId, force: true);
|
||||
} else {
|
||||
DbMessage? msg = await DbMessages.getMessageById(messageId);
|
||||
if (msg != null) {
|
||||
int index = _allMessagesFromUser[targetUserId]!
|
||||
.indexWhere((x) => x.messageId == messageId);
|
||||
if (index == -1) {
|
||||
print("should be indexed by time!!");
|
||||
|
||||
_allMessagesFromUser[targetUserId]!.insert(0, msg);
|
||||
// reload all messages but async
|
||||
loadMessagesForUser(targetUserId, force: true);
|
||||
} else {
|
||||
_allMessagesFromUser[targetUserId]![index] = msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future loadMessagesForUser(int targetUserId, {bool force = false}) async {
|
||||
if (!force && _allMessagesFromUser[targetUserId] != null) return;
|
||||
_allMessagesFromUser[targetUserId] =
|
||||
await DbMessages.getAllMessagesForUser(targetUserId);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void init({bool afterPaused = false}) async {
|
||||
// load everything from the database
|
||||
List<Contact> allContacts = await DbContacts.getUsers();
|
||||
for (Contact contact in allContacts) {
|
||||
DbMessage? last = await DbMessages.getLastMessagesForPreviewForUser(
|
||||
contact.userId.toInt());
|
||||
if (last != null) {
|
||||
_lastMessage[last.otherUserId] = last;
|
||||
}
|
||||
flamesCounter[contact.userId.toInt()] =
|
||||
await getFlamesForOtherUser(contact.userId.toInt());
|
||||
}
|
||||
notifyListeners();
|
||||
if (afterPaused) {
|
||||
for (int targetUserId in _allMessagesFromUser.keys) {
|
||||
loadMessagesForUser(targetUserId, force: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,8 @@ import 'package:firebase_messaging/firebase_messaging.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/app.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
import 'package:twonly/src/providers/api_provider.dart';
|
||||
import 'package:twonly/src/providers/db_provider.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
import '../../firebase_options.dart';
|
||||
|
|
@ -65,33 +65,26 @@ Future initFCMService() async {
|
|||
});
|
||||
}
|
||||
|
||||
late TwonlyDatabase bgTwonlyDB;
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
// If you're going to use other Firebase services in the background, such as Firestore,
|
||||
// make sure you call `initializeApp` before using other Firebase services.
|
||||
|
||||
// Wenn Tasks länger als 30 Sekunden ausgeführt werden, wird der Prozess möglicherweise automatisch vom Gerät beendet.
|
||||
// -> offer backend via http?
|
||||
|
||||
print("Handling a background message: ${message.messageId}");
|
||||
Logger("firebase-background")
|
||||
.shout('Handling a background message: ${message.messageId}');
|
||||
|
||||
bool gotMessage = false;
|
||||
bgTwonlyDB = TwonlyDatabase();
|
||||
|
||||
globalCallBackOnMessageChange = (a, b) async {
|
||||
gotMessage = true;
|
||||
print("Got message can exit");
|
||||
};
|
||||
|
||||
dbProvider = DbProvider();
|
||||
await dbProvider.ready;
|
||||
apiProvider = ApiProvider();
|
||||
await apiProvider.connect();
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
while (!gotMessage) {
|
||||
while (true) {
|
||||
if (stopwatch.elapsed >= Duration(seconds: 20)) {
|
||||
Logger("firebase-background").shout('Timeout reached. Exiting the loop.');
|
||||
break; // Exit the loop if the timeout is reached.
|
||||
Logger("firebase-background").shout('Exiting background handler');
|
||||
break;
|
||||
}
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
import 'package:twonly/src/model/json/message.dart' as my;
|
||||
|
||||
/// Streams are created so that app can respond to notification-related events
|
||||
|
|
@ -143,9 +142,9 @@ String getPushNotificationText(String key, String userName) {
|
|||
if (systemLanguage.contains("de")) {
|
||||
pushNotificationText = {
|
||||
"newTextMessage": "%userName% hat dir eine Nachricht gesendet.",
|
||||
"newTwonly": "%userName% hat dir einen twonly gesendet.",
|
||||
"newVideo": "%userName% hat dir eine Video gesendet.",
|
||||
"newImage": "%userName% hat dir eine Bild gesendet.",
|
||||
"newTwonly": "%userName% hat dir ein twonly gesendet.",
|
||||
"newVideo": "%userName% hat dir ein Video gesendet.",
|
||||
"newImage": "%userName% hat dir ein Bild gesendet.",
|
||||
"contactRequest": "%userName% möchte sich mir dir vernetzen.",
|
||||
"acceptRequest": "%userName% ist jetzt mit dir vernetzt.",
|
||||
};
|
||||
|
|
@ -166,7 +165,10 @@ String getPushNotificationText(String key, String userName) {
|
|||
|
||||
Future localPushNotificationNewMessage(
|
||||
int fromUserId, my.Message message, int messageId) async {
|
||||
Contact? user = await DbContacts.getUserById(fromUserId);
|
||||
Contact? user = await TwonlyDatabase.provider
|
||||
.getContactByUserId(fromUserId)
|
||||
.getSingleOrNull();
|
||||
|
||||
if (user == null) return;
|
||||
|
||||
String msg = "";
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
// The callback function should always be a top-level or static function.
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/providers/api_provider.dart';
|
||||
import 'package:twonly/src/providers/db_provider.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void startCallback() {
|
||||
FlutterForegroundTask.setTaskHandler(WebsocketForegroundTask());
|
||||
}
|
||||
|
||||
class WebsocketForegroundTask extends TaskHandler {
|
||||
// Called when the task is started.
|
||||
@override
|
||||
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {
|
||||
print('onStart(starter: ${starter.name})');
|
||||
|
||||
dbProvider = DbProvider();
|
||||
await dbProvider.ready;
|
||||
apiProvider = ApiProvider();
|
||||
apiProvider.connect();
|
||||
}
|
||||
|
||||
// Called based on the eventAction set in ForegroundTaskOptions.
|
||||
@override
|
||||
void onRepeatEvent(DateTime timestamp) {
|
||||
// Send data to main isolate.
|
||||
final Map<String, dynamic> data = {
|
||||
"timestampMillis": timestamp.millisecondsSinceEpoch,
|
||||
};
|
||||
FlutterForegroundTask.sendDataToMain(data);
|
||||
}
|
||||
|
||||
// Called when the task is destroyed.
|
||||
@override
|
||||
Future<void> onDestroy(DateTime timestamp) async {
|
||||
await apiProvider.close(() {});
|
||||
print('onDestroy');
|
||||
}
|
||||
|
||||
// Called when data is sent using `FlutterForegroundTask.sendDataToTask`.
|
||||
@override
|
||||
void onReceiveData(Object data) {
|
||||
apiProvider.close(() {});
|
||||
print('onReceiveData: $data');
|
||||
}
|
||||
|
||||
// Called when the notification button is pressed.
|
||||
@override
|
||||
void onNotificationButtonPressed(String id) {
|
||||
print('onNotificationButtonPressed: $id');
|
||||
}
|
||||
|
||||
// Called when the notification itself is pressed.
|
||||
@override
|
||||
void onNotificationPressed() {
|
||||
apiProvider.close(() {});
|
||||
}
|
||||
|
||||
// Called when the notification itself is dismissed.
|
||||
@override
|
||||
void onNotificationDismissed() {
|
||||
print('onNotificationDismissed');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,7 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
class Int64Converter implements JsonConverter<Int64, String> {
|
||||
const Int64Converter();
|
||||
|
||||
@override
|
||||
Int64 fromJson(String json) {
|
||||
return Int64.parseInt(json);
|
||||
}
|
||||
|
||||
@override
|
||||
String toJson(Int64 object) {
|
||||
return object.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class Uint8ListConverter implements JsonConverter<Uint8List, String> {
|
||||
const Uint8ListConverter();
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -9,13 +9,15 @@ import 'package:local_auth/local_auth.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||
import 'package:twonly/src/model/messages_model.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
import 'package:twonly/src/proto/api/error.pb.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
extension LocalizationExtension on BuildContext {
|
||||
extension ShortCutsExtension on BuildContext {
|
||||
AppLocalizations get lang => AppLocalizations.of(this)!;
|
||||
TwonlyDatabase get db => Provider.of<TwonlyDatabase>(this);
|
||||
}
|
||||
|
||||
// Function to check if a column exists
|
||||
|
|
@ -148,50 +150,48 @@ Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
|
|||
return result;
|
||||
}
|
||||
|
||||
int getFlameCounter(List<DateTime> dates) {
|
||||
if (dates.isEmpty) return 0;
|
||||
// int getFlameCounter(List<DateTime> dates) {
|
||||
// if (dates.isEmpty) return 0;
|
||||
|
||||
int flamesCount = 0;
|
||||
DateTime lastFlameCount = DateTime.now();
|
||||
// int flamesCount = 0;
|
||||
// DateTime lastFlameCount = DateTime.now();
|
||||
|
||||
if (calculateTimeDifference(dates[0], lastFlameCount).inDays == 0) {
|
||||
flamesCount = 1;
|
||||
lastFlameCount = dates[0];
|
||||
}
|
||||
// if (calculateTimeDifference(dates[0], lastFlameCount).inDays == 0) {
|
||||
// flamesCount = 1;
|
||||
// lastFlameCount = dates[0];
|
||||
// }
|
||||
|
||||
// print(dates[0]);
|
||||
for (int i = 1; i < dates.length; i++) {
|
||||
// print(
|
||||
// "${dates[i]} ${dates[i].difference(dates[i - 1]).inDays} ${dates[i].difference(lastFlameCount).inDays}");
|
||||
if (calculateTimeDifference(dates[i], dates[i - 1]).inDays == 0) {
|
||||
if (lastFlameCount.difference(dates[i]).inDays == 1) {
|
||||
flamesCount++;
|
||||
lastFlameCount = dates[i];
|
||||
}
|
||||
} else {
|
||||
break; // Stop counting if there's a break in the sequence
|
||||
}
|
||||
}
|
||||
return flamesCount;
|
||||
}
|
||||
// // print(dates[0]);
|
||||
// for (int i = 1; i < dates.length; i++) {
|
||||
// // print(
|
||||
// // "${dates[i]} ${dates[i].difference(dates[i - 1]).inDays} ${dates[i].difference(lastFlameCount).inDays}");
|
||||
// if (calculateTimeDifference(dates[i], dates[i - 1]).inDays == 0) {
|
||||
// if (lastFlameCount.difference(dates[i]).inDays == 1) {
|
||||
// flamesCount++;
|
||||
// lastFlameCount = dates[i];
|
||||
// }
|
||||
// } else {
|
||||
// break; // Stop counting if there's a break in the sequence
|
||||
// }
|
||||
// }
|
||||
// return flamesCount;
|
||||
// }
|
||||
|
||||
Future<int> getFlamesForOtherUser(int otherUserId) async {
|
||||
List<(DateTime, int?)> dates = await DbMessages.getMessageDates(otherUserId);
|
||||
// print("Dates ${dates.length}");
|
||||
if (dates.isEmpty) return 0;
|
||||
// Future<int> getFlamesForOtherUser(int otherUserId) async {
|
||||
// List<(DateTime, int?)> dates = await DbMessages.getMessageDates(otherUserId);
|
||||
// // print("Dates ${dates.length}");
|
||||
// if (dates.isEmpty) return 0;
|
||||
|
||||
List<DateTime> received =
|
||||
dates.where((x) => x.$2 != null).map((x) => x.$1).toList();
|
||||
List<DateTime> send =
|
||||
dates.where((x) => x.$2 == null).map((x) => x.$1).toList();
|
||||
// List<DateTime> received =
|
||||
// dates.where((x) => x.$2 != null).map((x) => x.$1).toList();
|
||||
// List<DateTime> send =
|
||||
// dates.where((x) => x.$2 == null).map((x) => x.$1).toList();
|
||||
|
||||
// print("Received ${received.length} and send ${send.length}");
|
||||
|
||||
int a = getFlameCounter(received);
|
||||
int b = getFlameCounter(send);
|
||||
// print("Received $a and send $b");
|
||||
return min(a, b);
|
||||
}
|
||||
// int a = getFlameCounter(received);
|
||||
// int b = getFlameCounter(send);
|
||||
// // print("Received $a and send $b");
|
||||
// return min(a, b);
|
||||
// }
|
||||
|
||||
Duration calculateTimeDifference(DateTime now, DateTime startTime) {
|
||||
// Get the timezone offsets
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ Future<ECPrivateKey?> getPrivateKey() async {
|
|||
}
|
||||
|
||||
Future<bool> addNewContact(Response_UserData userData) async {
|
||||
final Int64 userId = userData.userId;
|
||||
final int userId = userData.userId.toInt();
|
||||
|
||||
SignalProtocolAddress targetAddress =
|
||||
SignalProtocolAddress(userId.toString(), defaultDeviceId);
|
||||
|
|
@ -141,13 +141,14 @@ Future createIfNotExistsSignalIdentity() async {
|
|||
|
||||
final storedSignalIdentity = SignalIdentity(
|
||||
identityKeyPairU8List: identityKeyPair.serialize(),
|
||||
registrationId: Int64(registrationId));
|
||||
registrationId: registrationId,
|
||||
);
|
||||
|
||||
await storage.write(
|
||||
key: "signal_identity", value: jsonEncode(storedSignalIdentity));
|
||||
}
|
||||
|
||||
Future<Fingerprint?> generateSessionFingerPrint(Int64 target) async {
|
||||
Future<Fingerprint?> generateSessionFingerPrint(int target) async {
|
||||
ConnectSignalProtocolStore? signalStore = await getSignalStore();
|
||||
UserData? user = await getUser();
|
||||
if (signalStore == null || user == null) return null;
|
||||
|
|
@ -215,7 +216,7 @@ Future<Uint8List?> encryptBytes(Uint8List bytes, Int64 target) async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<Uint8List?> decryptBytes(Uint8List bytes, Int64 target) async {
|
||||
Future<Uint8List?> decryptBytes(Uint8List bytes, int target) async {
|
||||
try {
|
||||
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
|
||||
|
||||
|
|
@ -247,7 +248,7 @@ Future<Uint8List?> decryptBytes(Uint8List bytes, Int64 target) async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<Uint8List?> encryptMessage(Message msg, Int64 target) async {
|
||||
Future<Uint8List?> encryptMessage(Message msg, int target) async {
|
||||
try {
|
||||
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
|
||||
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
if (sendNextMediaToUserId != null) {
|
||||
Uint8List? imageBytes = await getMergedImage();
|
||||
sendImage(
|
||||
[Int64(sendNextMediaToUserId)],
|
||||
[sendNextMediaToUserId],
|
||||
imageBytes!,
|
||||
_isRealTwonly,
|
||||
_maxShowTime,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import 'package:twonly/src/components/flame.dart';
|
|||
import 'package:twonly/src/components/headline.dart';
|
||||
import 'package:twonly/src/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/components/verified_shield.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import '../../../../.blocked/archives/contacts_model.dart';
|
||||
import 'package:twonly/src/providers/api/api.dart';
|
||||
import 'package:twonly/src/providers/messages_change_provider.dart';
|
||||
import 'package:twonly/src/providers/send_next_media_to.dart';
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import 'package:twonly/src/components/animate_icon.dart';
|
|||
import 'package:twonly/src/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/components/message_send_state_icon.dart';
|
||||
import 'package:twonly/src/components/verified_shield.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import '../../../../.blocked/archives/contacts_model.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/messages_model.dart';
|
||||
import '../../../../.blocked/archives/messages_model.dart';
|
||||
import 'package:twonly/src/providers/api/api.dart';
|
||||
import 'package:twonly/src/providers/contacts_change_provider.dart';
|
||||
import 'package:twonly/src/providers/download_change_provider.dart';
|
||||
|
|
|
|||
|
|
@ -7,13 +7,11 @@ import 'package:twonly/src/components/initialsavatar.dart';
|
|||
import 'package:twonly/src/components/message_send_state_icon.dart';
|
||||
import 'package:twonly/src/components/notification_badge.dart';
|
||||
import 'package:twonly/src/components/user_context_menu.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/database/contacts_db.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
import 'package:twonly/src/database/messages_db.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/messages_model.dart';
|
||||
import 'package:twonly/src/providers/api/api.dart';
|
||||
import 'package:twonly/src/providers/contacts_change_provider.dart';
|
||||
import 'package:twonly/src/providers/download_change_provider.dart';
|
||||
import 'package:twonly/src/providers/messages_change_provider.dart';
|
||||
import 'package:twonly/src/providers/send_next_media_to.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/chats/chat_item_details_view.dart';
|
||||
|
|
@ -34,29 +32,7 @@ class ChatListView extends StatefulWidget {
|
|||
class _ChatListViewState extends State<ChatListView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Map<int, DbMessage> lastMessages =
|
||||
context.watch<MessagesChangeProvider>().lastMessage;
|
||||
|
||||
List<Contact> allUsers = context
|
||||
.watch<ContactChangeProvider>()
|
||||
.allContacts
|
||||
.where((c) => c.accepted)
|
||||
.toList();
|
||||
|
||||
allUsers.sort((b, a) {
|
||||
DbMessage? msgA = lastMessages[a.userId.toInt()];
|
||||
DbMessage? msgB = lastMessages[b.userId.toInt()];
|
||||
if (msgA == null) return 1;
|
||||
if (msgB == null) return -1;
|
||||
return msgA.sendAt.compareTo(msgB.sendAt);
|
||||
});
|
||||
|
||||
int maxTotalMediaCounter = 0;
|
||||
if (allUsers.isNotEmpty) {
|
||||
maxTotalMediaCounter = allUsers
|
||||
.map((x) => x.totalMediaCounter)
|
||||
.reduce((a, b) => a > b ? a : b);
|
||||
}
|
||||
Stream<List<Contact>> contacts = context.db.watchContactsForChatList();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
|
@ -73,11 +49,15 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
),
|
||||
// title:
|
||||
actions: [
|
||||
NotificationBadge(
|
||||
count: context
|
||||
.watch<ContactChangeProvider>()
|
||||
.newContactRequests
|
||||
.toString(),
|
||||
StreamBuilder(
|
||||
stream: context.db.watchContactsRequested(),
|
||||
builder: (context, snapshot) {
|
||||
var count = 0;
|
||||
if (snapshot.hasData && snapshot.data != null) {
|
||||
count = snapshot.data!;
|
||||
}
|
||||
return NotificationBadge(
|
||||
count: count.toString(),
|
||||
child: IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.userPlus, size: 18),
|
||||
onPressed: () {
|
||||
|
|
@ -89,6 +69,8 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
|
|
@ -103,8 +85,16 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
)
|
||||
],
|
||||
),
|
||||
body: (allUsers.isEmpty)
|
||||
? Center(
|
||||
body: StreamBuilder(
|
||||
stream: contacts,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final contacts = snapshot.data!;
|
||||
if (contacts.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: OutlinedButton.icon(
|
||||
|
|
@ -113,39 +103,42 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SearchUsernameView(),
|
||||
),
|
||||
);
|
||||
builder: (context) => SearchUsernameView()));
|
||||
},
|
||||
label: Text(context.lang.chatListViewSearchUserNameBtn)),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
);
|
||||
}
|
||||
|
||||
int maxTotalMediaCounter = 0;
|
||||
if (contacts.isNotEmpty) {
|
||||
maxTotalMediaCounter = contacts
|
||||
.map((x) => x.totalMediaCounter)
|
||||
.reduce((a, b) => a > b ? a : b);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
restorationId: 'chat_list_view',
|
||||
itemCount: allUsers.length,
|
||||
itemCount: contacts.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final user = allUsers[index];
|
||||
final user = contacts[index];
|
||||
return UserListItem(
|
||||
user: user,
|
||||
maxTotalMediaCounter: maxTotalMediaCounter,
|
||||
lastMessage: lastMessages[user.userId.toInt()],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class UserListItem extends StatefulWidget {
|
||||
final Contact user;
|
||||
final DbMessage? lastMessage;
|
||||
final int maxTotalMediaCounter;
|
||||
|
||||
const UserListItem(
|
||||
{super.key,
|
||||
required this.user,
|
||||
required this.lastMessage,
|
||||
required this.maxTotalMediaCounter});
|
||||
{super.key, required this.user, required this.maxTotalMediaCounter});
|
||||
|
||||
@override
|
||||
State<UserListItem> createState() => _UserListItem();
|
||||
|
|
@ -154,37 +147,37 @@ class UserListItem extends StatefulWidget {
|
|||
class _UserListItem extends State<UserListItem> {
|
||||
int lastMessageInSeconds = 0;
|
||||
MessageSendState state = MessageSendState.send;
|
||||
bool isDownloading = false;
|
||||
List<int> token = [];
|
||||
Message? currentMessage;
|
||||
|
||||
Timer? updateTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initAsync();
|
||||
// initAsync();
|
||||
lastUpdateTime();
|
||||
}
|
||||
|
||||
Future initAsync() async {
|
||||
if (widget.lastMessage != null) {
|
||||
if (!widget.lastMessage!.isDownloaded) {
|
||||
final content = widget.lastMessage!.messageContent;
|
||||
if (content is MediaMessageContent) {
|
||||
tryDownloadMedia(widget.lastMessage!.messageId,
|
||||
widget.lastMessage!.otherUserId, content.downloadToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Future initAsync() async {
|
||||
// if (currentMessage != null) {
|
||||
// if (currentMessage!.downloadState != DownloadState.downloading) {
|
||||
// final content = widget.lastMessage!.messageContent;
|
||||
// if (content is MediaMessageContent) {
|
||||
// tryDownloadMedia(widget.lastMessage!.messageId,
|
||||
// widget.lastMessage!.otherUserId, content.downloadToken);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
void lastUpdateTime() {
|
||||
// Change the color every 200 milliseconds
|
||||
updateTime = Timer.periodic(Duration(milliseconds: 200), (timer) {
|
||||
setState(() {
|
||||
if (widget.lastMessage != null) {
|
||||
lastMessageInSeconds = calculateTimeDifference(
|
||||
DateTime.now(), widget.lastMessage!.sendAt)
|
||||
if (currentMessage != null) {
|
||||
lastMessageInSeconds =
|
||||
calculateTimeDifference(DateTime.now(), currentMessage!.sendAt)
|
||||
.inSeconds;
|
||||
}
|
||||
});
|
||||
|
|
@ -199,35 +192,56 @@ class _UserListItem extends State<UserListItem> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.lastMessage != null) {
|
||||
state = widget.lastMessage!.getSendState();
|
||||
final notOpenedMessages =
|
||||
context.db.watchMessageNotOpened(widget.user.userId);
|
||||
final lastMessage = context.db.watchLastMessage(widget.user.userId);
|
||||
|
||||
final content = widget.lastMessage!.messageContent;
|
||||
// if (widget.lastMessage != null) {
|
||||
// state = widget.lastMessage!.getSendState();
|
||||
|
||||
if (widget.lastMessage!.messageReceived &&
|
||||
content is MediaMessageContent) {
|
||||
token = content.downloadToken;
|
||||
isDownloading = context
|
||||
.watch<DownloadChangeProvider>()
|
||||
.currentlyDownloading
|
||||
.contains(token.toString());
|
||||
}
|
||||
}
|
||||
// final content = widget.lastMessage!.messageContent;
|
||||
|
||||
int flameCounter = context
|
||||
.watch<MessagesChangeProvider>()
|
||||
.flamesCounter[widget.user.userId.toInt()] ??
|
||||
0;
|
||||
// if (widget.lastMessage!.messageReceived &&
|
||||
// content is MediaMessageContent) {
|
||||
// token = content.downloadToken;
|
||||
// isDownloading = context
|
||||
// .watch<DownloadChangeProvider>()
|
||||
// .currentlyDownloading
|
||||
// .contains(token.toString());
|
||||
// }
|
||||
// }
|
||||
|
||||
int flameCounter = getFlameCounterFromContact(widget.user);
|
||||
|
||||
return UserContextMenu(
|
||||
user: widget.user,
|
||||
contact: widget.user,
|
||||
child: ListTile(
|
||||
title: Text(widget.user.displayName),
|
||||
subtitle: (widget.lastMessage == null)
|
||||
? Text(context.lang.chatsTapToSend)
|
||||
: Row(
|
||||
title: Text(getContactDisplayName(widget.user)),
|
||||
subtitle: StreamBuilder(
|
||||
stream: lastMessage,
|
||||
builder: (context, lastMessageSnapshot) {
|
||||
if (!lastMessageSnapshot.hasData) {
|
||||
return Container();
|
||||
}
|
||||
if (lastMessageSnapshot.data == null) {
|
||||
return Text(context.lang.chatsTapToSend);
|
||||
}
|
||||
final lastMessage = lastMessageSnapshot.data!;
|
||||
return StreamBuilder(
|
||||
stream: notOpenedMessages,
|
||||
builder: (context, notOpenedMessagesSnapshot) {
|
||||
if (!lastMessageSnapshot.hasData) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
var lastMessages = [lastMessage];
|
||||
if (notOpenedMessagesSnapshot.data != null) {
|
||||
lastMessages = notOpenedMessagesSnapshot.data!;
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
MessageSendStateIcon(widget.lastMessage!),
|
||||
MessageSendStateIcon(lastMessages),
|
||||
Text("•"),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
|
|
@ -242,29 +256,34 @@ class _UserListItem extends State<UserListItem> {
|
|||
prefix: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
leading: InitialsAvatar(displayName: widget.user.displayName),
|
||||
leading: InitialsAvatar(getContactDisplayName(widget.user)),
|
||||
onTap: () {
|
||||
if (widget.lastMessage == null) {
|
||||
if (currentMessage == null) {
|
||||
context
|
||||
.read<SendNextMediaTo>()
|
||||
.updateSendNextMediaTo(widget.user.userId.toInt());
|
||||
globalUpdateOfHomeViewPageIndex(0);
|
||||
return;
|
||||
}
|
||||
if (isDownloading) return;
|
||||
if (!widget.lastMessage!.isDownloaded) {
|
||||
tryDownloadMedia(widget.lastMessage!.messageId,
|
||||
widget.lastMessage!.otherUserId, token,
|
||||
force: true);
|
||||
Message msg = currentMessage!;
|
||||
if (msg.downloadState == DownloadState.downloading) {
|
||||
return;
|
||||
}
|
||||
if (msg.downloadState == DownloadState.pending) {
|
||||
tryDownloadMedia(msg.messageId, msg.contactId, token, force: true);
|
||||
return;
|
||||
}
|
||||
if (state == MessageSendState.received &&
|
||||
widget.lastMessage!.containsOtherMedia()) {
|
||||
msg.kind == MessageKind.media) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return MediaViewerView(widget.user, widget.lastMessage!);
|
||||
return MediaViewerView(widget.user, msg);
|
||||
}),
|
||||
);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import 'package:no_screenshot/no_screenshot.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/src/components/animate_icon.dart';
|
||||
import 'package:twonly/src/components/media_view_sizing.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import '../../../../.blocked/archives/contacts_model.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/messages_model.dart';
|
||||
import '../../../../.blocked/archives/messages_model.dart';
|
||||
import 'package:twonly/src/providers/api/api.dart';
|
||||
import 'package:twonly/src/providers/messages_change_provider.dart';
|
||||
import 'package:twonly/src/providers/send_next_media_to.dart';
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import 'dart:async';
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/database/contacts_db.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/components/headline.dart';
|
||||
import 'package:twonly/src/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/providers/contacts_change_provider.dart';
|
||||
import 'package:twonly/src/providers/api/api.dart';
|
||||
// ignore: library_prefixes
|
||||
import 'package:twonly/src/utils/signal.dart' as SignalHelper;
|
||||
|
|
@ -31,18 +31,28 @@ class _SearchUsernameView extends State<SearchUsernameView> {
|
|||
|
||||
final res = await apiProvider.getUserData(searchUserName.text);
|
||||
|
||||
if (res.isSuccess) {
|
||||
bool added = await DbContacts.insertNewContact(
|
||||
searchUserName.text,
|
||||
res.value.userdata.userId.toInt(),
|
||||
false,
|
||||
);
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (added) {
|
||||
if (res.isSuccess) {
|
||||
final addUser = await showAlertDialog(
|
||||
context, "User found", "Do you want to create a follow request?");
|
||||
if (!addUser || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
int added = await context.db.insertContact(ContactsCompanion(
|
||||
username: Value(searchUserName.text),
|
||||
userId: Value(res.value.userdata.userId),
|
||||
requested: Value(false),
|
||||
));
|
||||
|
||||
if (added > 0) {
|
||||
if (await SignalHelper.addNewContact(res.value.userdata)) {
|
||||
encryptAndSendMessage(
|
||||
res.value.userdata.userId,
|
||||
Message(
|
||||
MessageJson(
|
||||
kind: MessageKind.contactRequest,
|
||||
timestamp: DateTime.now(),
|
||||
content: MessageContent(),
|
||||
|
|
@ -50,7 +60,7 @@ class _SearchUsernameView extends State<SearchUsernameView> {
|
|||
);
|
||||
}
|
||||
}
|
||||
} else if (context.mounted) {
|
||||
} else {
|
||||
showAlertDialog(context, context.lang.searchUsernameNotFound,
|
||||
context.lang.searchUsernameNotFoundBody(searchUserName.text));
|
||||
}
|
||||
|
|
@ -79,6 +89,8 @@ class _SearchUsernameView extends State<SearchUsernameView> {
|
|||
);
|
||||
}
|
||||
|
||||
Stream<List<Contact>> contacts = context.db.watchNotAcceptedContacts();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.searchUsernameTitle),
|
||||
|
|
@ -108,14 +120,24 @@ class _SearchUsernameView extends State<SearchUsernameView> {
|
|||
label: Text(context.lang.searchUsernameQrCodeBtn),
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
if (context
|
||||
.watch<ContactChangeProvider>()
|
||||
.allContacts
|
||||
.where((contact) => !contact.accepted)
|
||||
.isNotEmpty)
|
||||
HeadLineComponent(context.lang.searchUsernameNewFollowerTitle),
|
||||
StreamBuilder(
|
||||
stream: contacts,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data != null) {
|
||||
return Container();
|
||||
}
|
||||
final contacts = snapshot.data!;
|
||||
if (contacts.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
return Row(children: [
|
||||
HeadLineComponent(
|
||||
context.lang.searchUsernameNewFollowerTitle),
|
||||
Expanded(
|
||||
child: ContactsListView(),
|
||||
child: ContactsListView(contacts),
|
||||
)
|
||||
]);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
|
|
@ -136,7 +158,9 @@ class _SearchUsernameView extends State<SearchUsernameView> {
|
|||
}
|
||||
|
||||
class ContactsListView extends StatefulWidget {
|
||||
const ContactsListView({super.key});
|
||||
const ContactsListView(this.contacts, {super.key});
|
||||
|
||||
final List<Contact> contacts;
|
||||
|
||||
@override
|
||||
State<ContactsListView> createState() => _ContactsListViewState();
|
||||
|
|
@ -145,18 +169,14 @@ class ContactsListView extends StatefulWidget {
|
|||
class _ContactsListViewState extends State<ContactsListView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Contact> contacts = context
|
||||
.read<ContactChangeProvider>()
|
||||
.allContacts
|
||||
.where((contact) => !contact.accepted)
|
||||
.toList();
|
||||
return ListView.builder(
|
||||
itemCount: contacts.length,
|
||||
itemCount: widget.contacts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final contact = contacts[index];
|
||||
final contact = widget.contacts[index];
|
||||
final displayName = getContactDisplayName(contact);
|
||||
return ListTile(
|
||||
title: Text(contact.displayName),
|
||||
leading: InitialsAvatar(displayName: contact.displayName),
|
||||
title: Text(displayName),
|
||||
leading: InitialsAvatar(displayName),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
|
@ -168,7 +188,8 @@ class _ContactsListViewState extends State<ContactsListView> {
|
|||
icon: Icon(Icons.person_off_rounded,
|
||||
color: const Color.fromARGB(164, 244, 67, 54)),
|
||||
onPressed: () async {
|
||||
await DbContacts.blockUser(contact.userId.toInt());
|
||||
final update = ContactsCompanion(blocked: Value(true));
|
||||
await context.db.updateContact(contact.userId, update);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
@ -177,10 +198,10 @@ class _ContactsListViewState extends State<ContactsListView> {
|
|||
child: IconButton(
|
||||
icon: Icon(Icons.close, color: Colors.red),
|
||||
onPressed: () async {
|
||||
await DbContacts.deleteUser(contact.userId.toInt());
|
||||
await context.db.deleteContactByUserId(contact.userId);
|
||||
encryptAndSendMessage(
|
||||
contact.userId,
|
||||
Message(
|
||||
MessageJson(
|
||||
kind: MessageKind.rejectRequest,
|
||||
timestamp: DateTime.now(),
|
||||
content: MessageContent(),
|
||||
|
|
@ -192,10 +213,11 @@ class _ContactsListViewState extends State<ContactsListView> {
|
|||
IconButton(
|
||||
icon: Icon(Icons.check, color: Colors.green),
|
||||
onPressed: () async {
|
||||
await DbContacts.acceptUser(contact.userId.toInt());
|
||||
final update = ContactsCompanion(accepted: Value(true));
|
||||
await context.db.updateContact(contact.userId, update);
|
||||
encryptAndSendMessage(
|
||||
contact.userId,
|
||||
Message(
|
||||
MessageJson(
|
||||
kind: MessageKind.acceptRequest,
|
||||
timestamp: DateTime.now(),
|
||||
content: MessageContent(),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import 'dart:convert';
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:twonly/src/components/format_long_string.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/providers/contacts_change_provider.dart';
|
||||
import 'package:twonly/src/database/contacts_db.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/signal.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
|
@ -36,10 +36,9 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Contact contact = context
|
||||
.watch<ContactChangeProvider>()
|
||||
.allContacts
|
||||
.firstWhere((c) => c.userId == widget.contact.userId);
|
||||
Stream<Contact?> contact = context.db
|
||||
.getContactByUserId(widget.contact.userId)
|
||||
.watchSingleOrNull();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
|
@ -92,13 +91,21 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
StreamBuilder(
|
||||
stream: contact,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data == null) {
|
||||
return Container();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30),
|
||||
child: Text(
|
||||
context.lang
|
||||
.contactVerifyNumberLongDesc(contact.displayName),
|
||||
context.lang.contactVerifyNumberLongDesc(
|
||||
getContactDisplayName(snapshot.data!)),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
|
|
@ -125,23 +132,33 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
contact.verified
|
||||
? OutlinedButton.icon(
|
||||
StreamBuilder(
|
||||
stream: contact,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data == null) {
|
||||
return Container();
|
||||
}
|
||||
final contact = snapshot.data!;
|
||||
if (contact.verified) {
|
||||
return OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
DbContacts.updateVerificationStatus(
|
||||
contact.userId.toInt(), false);
|
||||
final update =
|
||||
ContactsCompanion(verified: Value(false));
|
||||
context.db.updateContact(contact.userId, update);
|
||||
},
|
||||
label: Text(
|
||||
context.lang.contactVerifyNumberClearVerification),
|
||||
)
|
||||
: FilledButton.icon(
|
||||
);
|
||||
}
|
||||
return FilledButton.icon(
|
||||
icon: FaIcon(FontAwesomeIcons.shieldHeart),
|
||||
onPressed: () {
|
||||
DbContacts.updateVerificationStatus(
|
||||
contact.userId.toInt(), true);
|
||||
final update = ContactsCompanion(verified: Value(true));
|
||||
context.db.updateContact(contact.userId, update);
|
||||
},
|
||||
label: Text(context.lang.contactVerifyNumberMarkAsVerified),
|
||||
);
|
||||
},
|
||||
label:
|
||||
Text(context.lang.contactVerifyNumberMarkAsVerified),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/src/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/components/better_list_title.dart';
|
||||
import 'package:twonly/src/components/flame.dart';
|
||||
import 'package:twonly/src/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/components/verified_shield.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/providers/contacts_change_provider.dart';
|
||||
import 'package:twonly/src/providers/messages_change_provider.dart';
|
||||
import 'package:twonly/src/database/contacts_db.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/contact/contact_verify_view.dart';
|
||||
|
||||
|
|
@ -24,26 +23,27 @@ class ContactView extends StatefulWidget {
|
|||
class _ContactViewState extends State<ContactView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Contact contact = context
|
||||
.watch<ContactChangeProvider>()
|
||||
.allContacts
|
||||
.firstWhere((c) => c.userId == widget.userId);
|
||||
|
||||
int flameCounter = context
|
||||
.watch<MessagesChangeProvider>()
|
||||
.flamesCounter[contact.userId.toInt()] ??
|
||||
0;
|
||||
Stream<Contact?> contact =
|
||||
context.db.getContactByUserId(widget.userId).watchSingleOrNull();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(""),
|
||||
),
|
||||
body: ListView(
|
||||
body: StreamBuilder(
|
||||
stream: contact,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data == null) {
|
||||
return Container();
|
||||
}
|
||||
final contact = snapshot.data!;
|
||||
int flameCounter = getFlameCounterFromContact(contact);
|
||||
return ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: InitialsAvatar(
|
||||
displayName: contact.displayName,
|
||||
getContactDisplayName(contact),
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
|
|
@ -54,7 +54,7 @@ class _ContactViewState extends State<ContactView> {
|
|||
padding: EdgeInsets.only(right: 10),
|
||||
child: VerifiedShield(contact)),
|
||||
Text(
|
||||
contact.displayName,
|
||||
getContactDisplayName(contact),
|
||||
style: TextStyle(fontSize: 20),
|
||||
),
|
||||
if (flameCounter > 0)
|
||||
|
|
@ -71,11 +71,12 @@ class _ContactViewState extends State<ContactView> {
|
|||
icon: FontAwesomeIcons.pencil,
|
||||
text: context.lang.contactNickname,
|
||||
onTap: () async {
|
||||
String? newUsername =
|
||||
String? nickName =
|
||||
await showNicknameChangeDialog(context, contact);
|
||||
if (newUsername != null && newUsername != "") {
|
||||
await DbContacts.changeDisplayName(
|
||||
contact.userId.toInt(), newUsername);
|
||||
|
||||
if (context.mounted && nickName != null && nickName != "") {
|
||||
final update = ContactsCompanion(nickName: Value(nickName));
|
||||
context.db.updateContact(contact.userId, update);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
@ -98,12 +99,15 @@ class _ContactViewState extends State<ContactView> {
|
|||
onTap: () async {
|
||||
bool block = await showAlertDialog(
|
||||
context,
|
||||
context.lang.contactBlockTitle(contact.displayName),
|
||||
context.lang
|
||||
.contactBlockTitle(getContactDisplayName(contact)),
|
||||
context.lang.contactBlockBody,
|
||||
);
|
||||
if (block) {
|
||||
await DbContacts.blockUser(contact.userId.toInt());
|
||||
// go back to the first
|
||||
final update = ContactsCompanion(blocked: Value(true));
|
||||
if (context.mounted) {
|
||||
await context.db.updateContact(contact.userId, update);
|
||||
}
|
||||
if (context.mounted) {
|
||||
Navigator.popUntil(context, (route) => route.isFirst);
|
||||
}
|
||||
|
|
@ -111,6 +115,8 @@ class _ContactViewState extends State<ContactView> {
|
|||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/settings/privacy_view_block_users.dart';
|
||||
|
||||
|
|
@ -11,17 +10,9 @@ class PrivacyView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _PrivacyViewState extends State<PrivacyView> {
|
||||
List<Contact> blockedUsers = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
updateBlockedUsers();
|
||||
}
|
||||
|
||||
Future updateBlockedUsers() async {
|
||||
blockedUsers = await DbContacts.getBlockedUsers();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -34,15 +25,25 @@ class _PrivacyViewState extends State<PrivacyView> {
|
|||
children: [
|
||||
ListTile(
|
||||
title: Text(context.lang.settingsPrivacyBlockUsers),
|
||||
subtitle: Text(
|
||||
context.lang.settingsPrivacyBlockUsersCount(blockedUsers.length),
|
||||
subtitle: StreamBuilder(
|
||||
stream: context.db.watchContactsBlocked(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data != null) {
|
||||
return Text(
|
||||
context.lang.settingsPrivacyBlockUsersCount(snapshot.data!),
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
context.lang.settingsPrivacyBlockUsersCount(0),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () async {
|
||||
await Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return PrivacyViewBlockUsers();
|
||||
}));
|
||||
updateBlockedUsers();
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/database/contacts_db.dart';
|
||||
import 'package:twonly/src/database/database.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class PrivacyViewBlockUsers extends StatefulWidget {
|
||||
|
|
@ -11,35 +13,34 @@ class PrivacyViewBlockUsers extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
|
||||
List<Contact> allUsers = [];
|
||||
late Stream<List<Contact>> allUsers;
|
||||
List<Contact> filteredUsers = [];
|
||||
String lastQuery = "";
|
||||
String filter = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
allUsers = context.db.watchAllContacts();
|
||||
loadAsync();
|
||||
}
|
||||
|
||||
Future loadAsync() async {
|
||||
allUsers = await DbContacts.getAllUsers();
|
||||
_filterUsers(lastQuery);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future _filterUsers(String query) async {
|
||||
lastQuery = query;
|
||||
if (query.isEmpty) {
|
||||
filteredUsers = allUsers;
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
filteredUsers = allUsers
|
||||
.where((user) =>
|
||||
user.displayName.toLowerCase().contains(query.toLowerCase()))
|
||||
.toList();
|
||||
setState(() {});
|
||||
}
|
||||
// Future _filterUsers(String query) async {
|
||||
// lastQuery = query;
|
||||
// if (query.isEmpty) {
|
||||
// filteredUsers = allUsers;
|
||||
// setState(() {});
|
||||
// return;
|
||||
// }
|
||||
// filteredUsers = allUsers
|
||||
// .where((user) =>
|
||||
// user.displayName.toLowerCase().contains(query.toLowerCase()))
|
||||
// .toList();
|
||||
// setState(() {});
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -54,7 +55,9 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
|
|||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: TextField(
|
||||
onChanged: _filterUsers,
|
||||
onChanged: (value) => setState(() {
|
||||
filter = value;
|
||||
}),
|
||||
decoration: getInputDecoration(
|
||||
context,
|
||||
context.lang.searchUsernameInput,
|
||||
|
|
@ -68,10 +71,22 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
|
|||
),
|
||||
const SizedBox(height: 30),
|
||||
Expanded(
|
||||
child: UserList(
|
||||
List.from(filteredUsers),
|
||||
updateStatus: () {
|
||||
loadAsync();
|
||||
child: StreamBuilder(
|
||||
stream: allUsers,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final filteredContacts = snapshot.data!.where((contact) {
|
||||
return getContactDisplayName(contact)
|
||||
.toLowerCase()
|
||||
.contains(filter.toLowerCase());
|
||||
}).toList();
|
||||
|
||||
return UserList(
|
||||
List.from(filteredContacts),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
@ -83,20 +98,21 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
|
|||
}
|
||||
|
||||
class UserList extends StatelessWidget {
|
||||
const UserList(this.users, {super.key, required this.updateStatus});
|
||||
const UserList(this.users, {super.key});
|
||||
final List<Contact> users;
|
||||
final Function updateStatus;
|
||||
|
||||
Future block(bool? value, int userId) async {
|
||||
if (value == null) return;
|
||||
await DbContacts.blockUser(userId, unblock: !value);
|
||||
updateStatus();
|
||||
Future block(BuildContext context, int userId, bool? value) async {
|
||||
if (value != null) {
|
||||
final update = ContactsCompanion(blocked: Value(!value));
|
||||
await context.db.updateContact(userId, update);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Step 1: Sort the users alphabetically
|
||||
users.sort((a, b) => a.displayName.compareTo(b.displayName));
|
||||
users.sort(
|
||||
(a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)));
|
||||
|
||||
return ListView.builder(
|
||||
restorationId: 'new_message_users_list',
|
||||
|
|
@ -105,20 +121,20 @@ class UserList extends StatelessWidget {
|
|||
Contact user = users[i];
|
||||
return ListTile(
|
||||
title: Row(children: [
|
||||
Text(user.displayName),
|
||||
Text(getContactDisplayName(user)),
|
||||
]),
|
||||
leading: InitialsAvatar(
|
||||
displayName: user.displayName,
|
||||
getContactDisplayName(user),
|
||||
fontSize: 15,
|
||||
),
|
||||
trailing: Checkbox(
|
||||
value: user.blocked,
|
||||
onChanged: (bool? value) {
|
||||
block(value, user.userId.toInt());
|
||||
block(context, user.userId, value);
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
block(!user.blocked, user.userId.toInt());
|
||||
block(context, user.userId, !user.blocked);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class _ProfileViewState extends State<ProfileView> {
|
|||
child: Row(
|
||||
children: [
|
||||
InitialsAvatar(
|
||||
displayName: userData!.username,
|
||||
userData!.username,
|
||||
fontSize: 30,
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
|
|
|
|||
72
pubspec.lock
72
pubspec.lock
|
|
@ -93,18 +93,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e"
|
||||
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
version: "2.4.4"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
|
||||
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.14"
|
||||
version: "2.4.15"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -153,6 +153,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -273,6 +281,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
drift:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "97d5832657d49f26e7a8e07de397ddc63790b039372878d5117af816d0fdb5cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.25.1"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: f1db88482dbb016b9bbddddf746d5d0a6938b156ff20e07320052981f97388cc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.25.2"
|
||||
drift_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift_flutter
|
||||
sha256: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.4"
|
||||
ed25519_edwards:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1133,6 +1165,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: recase
|
||||
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
reorderables:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1290,6 +1330,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0+1"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "32b632dda27d664f85520093ed6f735ae5c49b5b75345afb8b19411bc59bb53d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.4"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.31"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.41.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ dependencies:
|
|||
collection: ^1.18.0
|
||||
connectivity_plus: ^6.1.2
|
||||
cv: ^1.1.3
|
||||
drift: ^2.25.1
|
||||
drift_flutter: ^0.2.4
|
||||
exif: ^3.3.0
|
||||
firebase_core: ^3.11.0
|
||||
firebase_messaging: ^15.2.2
|
||||
|
|
@ -60,10 +62,11 @@ dependencies:
|
|||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.3.3
|
||||
build_runner: ^2.4.15
|
||||
json_serializable: ^6.8.0
|
||||
flutter_lints: ^5.0.0
|
||||
flutter_launcher_icons: ^0.14.1
|
||||
drift_dev: ^2.25.2
|
||||
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
|
|
|
|||
Loading…
Reference in a new issue