mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 10:38:41 +00:00
fix #132
This commit is contained in:
parent
6d5be8c52f
commit
cbc3faba4e
15 changed files with 638 additions and 172 deletions
|
|
@ -235,5 +235,16 @@
|
||||||
"planNotAllowed": "In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.",
|
"planNotAllowed": "In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.",
|
||||||
"planLimitReached": "Du hast dein Planlimit für heute erreicht. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.",
|
"planLimitReached": "Du hast dein Planlimit für heute erreicht. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.",
|
||||||
"galleryDelete": "Datei löschen",
|
"galleryDelete": "Datei löschen",
|
||||||
"galleryDetails": "Details anzeigen"
|
"galleryDetails": "Details anzeigen",
|
||||||
|
"settingsResetTutorials": "Tutorials erneut anzeigen",
|
||||||
|
"settingsResetTutorialsDesc": "Klicke hier, um bereits angezeigte Tutorials erneut anzuzeigen.",
|
||||||
|
"settingsResetTutorialsSuccess": "Tutorials werden erneut angezeigt.",
|
||||||
|
"tutorialChatListSearchUsersTitle": "Freunde finden und Freundschaftsanfragen verwalten",
|
||||||
|
"tutorialChatListSearchUsersDesc": "Wenn du die Benutzernamen deiner Freunde kennst, kannst du sie hier suchen und eine Freundschaftsanfrage senden. Außerdem siehst du hier alle Anfragen von anderen Nutzern, die du annehmen oder blockieren kannst.",
|
||||||
|
"tutorialChatListContextMenuTitle": "Klicke lange auf den Kontakt, um das Kontextmenü zu öffnen.",
|
||||||
|
"tutorialChatListContextMenuDesc": "Mit dem Kontextmenü kannst du deine Kontakte anheften, archivieren und verschiedene Aktionen durchführen. Halte dazu einfach den Kontakt lange gedrückt und bewege dann deinen Finger auf die gewünschte Option oder tippe direkt darauf.",
|
||||||
|
"tutorialChatMessagesVerifyShieldTitle": "Verifiziere deine Kontakte!",
|
||||||
|
"tutorialChatMessagesVerifyShieldDesc": "twonly nutzt das Signal-Protokoll für eine sichere Ende-zu-Ende Verschlüsselung. Bei der ersten Kontaktaufnahme wird dafür der öffentliche Identitätsschlüssel von deinem Kontakt heruntergeladen. Um sicherzustellen, dass dieser Schlüssel nicht von Dritten ausgetauscht wurde, solltest du ihn mit deinem Freund vergleichen, wenn ihr euch persönlich trefft. Sobald du den Benutzer verifiziert hast, kannst du auch beim verschicken von Bildern und Videos den twonly-Modus aktivieren.",
|
||||||
|
"tutorialChatMessagesReopenMessageTitle": "Bilder und Videos erneut öffnen",
|
||||||
|
"tutorialChatMessagesReopenMessageDesc": "Wenn dein Freund dir ein Bild oder Video mit unendlicher Anzeigezeit gesendet hat, kannst du es bis zum Neustart der App jederzeit erneut öffnen. Um dies zu tun, musst du einfach doppelt auf die Nachricht klicken. Dein Freund erhält dann eine Benachrichtigung, dass du das Bild erneut angesehen hast."
|
||||||
}
|
}
|
||||||
|
|
@ -394,5 +394,16 @@
|
||||||
"planLimitReached": "You have reached your plan limit for today. Upgrade your plan now to send the media file.",
|
"planLimitReached": "You have reached your plan limit for today. Upgrade your plan now to send the media file.",
|
||||||
"planNotAllowed": "You cannot send media files with your current tariff. Upgrade your plan now to send the media file.",
|
"planNotAllowed": "You cannot send media files with your current tariff. Upgrade your plan now to send the media file.",
|
||||||
"galleryDelete": "Delete file",
|
"galleryDelete": "Delete file",
|
||||||
"galleryDetails": "Show details"
|
"galleryDetails": "Show details",
|
||||||
|
"settingsResetTutorials": "Show tutorials again",
|
||||||
|
"settingsResetTutorialsDesc": "Click here to show already displayed tutorials again.",
|
||||||
|
"settingsResetTutorialsSuccess": "Tutorials will be displayed again.",
|
||||||
|
"tutorialChatListSearchUsersTitle": "Find Friends and Manage Friend Requests",
|
||||||
|
"tutorialChatListSearchUsersDesc": "If you know your friends' usernames, you can search for them here and send a friend request. You will also see all requests from other users that you can accept or block.",
|
||||||
|
"tutorialChatListContextMenuTitle": "Long press on the contact to open the context menu.",
|
||||||
|
"tutorialChatListContextMenuDesc": "With the context menu, you can pin, archive, and perform various actions on your contacts. Simply long press the contact and then move your finger to the desired option or tap directly on it.",
|
||||||
|
"tutorialChatMessagesVerifyShieldTitle": "Verify your contacts!",
|
||||||
|
"tutorialChatMessagesVerifyShieldDesc": "twonly uses the Signal protocol for secure end-to-end encryption. When you first contact someone, their public identity key is downloaded. To ensure that this key has not been tampered with by third parties, you should compare it with your friend when you meet in person. Once you have verified the user, you can also enable the twonly mode when sending images and videos.",
|
||||||
|
"tutorialChatMessagesReopenMessageTitle": "Reopen Images and Videos",
|
||||||
|
"tutorialChatMessagesReopenMessageDesc": "If your friend has sent you a picture or video with infinite display time, you can open it again at any time until you restart the app. To do this, simply double-click on the message. Your friend will then receive a notification that you have viewed the picture again."
|
||||||
}
|
}
|
||||||
|
|
@ -1429,6 +1429,72 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Show details'**
|
/// **'Show details'**
|
||||||
String get galleryDetails;
|
String get galleryDetails;
|
||||||
|
|
||||||
|
/// No description provided for @settingsResetTutorials.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show tutorials again'**
|
||||||
|
String get settingsResetTutorials;
|
||||||
|
|
||||||
|
/// No description provided for @settingsResetTutorialsDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Click here to show already displayed tutorials again.'**
|
||||||
|
String get settingsResetTutorialsDesc;
|
||||||
|
|
||||||
|
/// No description provided for @settingsResetTutorialsSuccess.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Tutorials will be displayed again.'**
|
||||||
|
String get settingsResetTutorialsSuccess;
|
||||||
|
|
||||||
|
/// No description provided for @tutorialChatListSearchUsersTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Find Friends and Manage Friend Requests'**
|
||||||
|
String get tutorialChatListSearchUsersTitle;
|
||||||
|
|
||||||
|
/// No description provided for @tutorialChatListSearchUsersDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'If you know your friends\' usernames, you can search for them here and send a friend request. You will also see all requests from other users that you can accept or block.'**
|
||||||
|
String get tutorialChatListSearchUsersDesc;
|
||||||
|
|
||||||
|
/// No description provided for @tutorialChatListContextMenuTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Long press on the contact to open the context menu.'**
|
||||||
|
String get tutorialChatListContextMenuTitle;
|
||||||
|
|
||||||
|
/// No description provided for @tutorialChatListContextMenuDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'With the context menu, you can pin, archive, and perform various actions on your contacts. Simply long press the contact and then move your finger to the desired option or tap directly on it.'**
|
||||||
|
String get tutorialChatListContextMenuDesc;
|
||||||
|
|
||||||
|
/// No description provided for @tutorialChatMessagesVerifyShieldTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Verify your contacts!'**
|
||||||
|
String get tutorialChatMessagesVerifyShieldTitle;
|
||||||
|
|
||||||
|
/// No description provided for @tutorialChatMessagesVerifyShieldDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'twonly uses the Signal protocol for secure end-to-end encryption. When you first contact someone, their public identity key is downloaded. To ensure that this key has not been tampered with by third parties, you should compare it with your friend when you meet in person. Once you have verified the user, you can also enable the twonly mode when sending images and videos.'**
|
||||||
|
String get tutorialChatMessagesVerifyShieldDesc;
|
||||||
|
|
||||||
|
/// No description provided for @tutorialChatMessagesReopenMessageTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reopen Images and Videos'**
|
||||||
|
String get tutorialChatMessagesReopenMessageTitle;
|
||||||
|
|
||||||
|
/// No description provided for @tutorialChatMessagesReopenMessageDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'If your friend has sent you a picture or video with infinite display time, you can open it again at any time until you restart the app. To do this, simply double-click on the message. Your friend will then receive a notification that you have viewed the picture again.'**
|
||||||
|
String get tutorialChatMessagesReopenMessageDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -743,4 +743,47 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get galleryDetails => 'Details anzeigen';
|
String get galleryDetails => 'Details anzeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsResetTutorials => 'Tutorials erneut anzeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsResetTutorialsDesc =>
|
||||||
|
'Klicke hier, um bereits angezeigte Tutorials erneut anzuzeigen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsResetTutorialsSuccess =>
|
||||||
|
'Tutorials werden erneut angezeigt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatListSearchUsersTitle =>
|
||||||
|
'Freunde finden und Freundschaftsanfragen verwalten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatListSearchUsersDesc =>
|
||||||
|
'Wenn du die Benutzernamen deiner Freunde kennst, kannst du sie hier suchen und eine Freundschaftsanfrage senden. Außerdem siehst du hier alle Anfragen von anderen Nutzern, die du annehmen oder blockieren kannst.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatListContextMenuTitle =>
|
||||||
|
'Klicke lange auf den Kontakt, um das Kontextmenü zu öffnen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatListContextMenuDesc =>
|
||||||
|
'Mit dem Kontextmenü kannst du deine Kontakte anheften, archivieren und verschiedene Aktionen durchführen. Halte dazu einfach den Kontakt lange gedrückt und bewege dann deinen Finger auf die gewünschte Option oder tippe direkt darauf.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatMessagesVerifyShieldTitle =>
|
||||||
|
'Verifiziere deine Kontakte!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatMessagesVerifyShieldDesc =>
|
||||||
|
'twonly nutzt das Signal-Protokoll für eine sichere Ende-zu-Ende Verschlüsselung. Bei der ersten Kontaktaufnahme wird dafür der öffentliche Identitätsschlüssel von deinem Kontakt heruntergeladen. Um sicherzustellen, dass dieser Schlüssel nicht von Dritten ausgetauscht wurde, solltest du ihn mit deinem Freund vergleichen, wenn ihr euch persönlich trefft. Sobald du den Benutzer verifiziert hast, kannst du auch beim verschicken von Bildern und Videos den twonly-Modus aktivieren.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatMessagesReopenMessageTitle =>
|
||||||
|
'Bilder und Videos erneut öffnen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatMessagesReopenMessageDesc =>
|
||||||
|
'Wenn dein Freund dir ein Bild oder Video mit unendlicher Anzeigezeit gesendet hat, kannst du es bis zum Neustart der App jederzeit erneut öffnen. Um dies zu tun, musst du einfach doppelt auf die Nachricht klicken. Dein Freund erhält dann eine Benachrichtigung, dass du das Bild erneut angesehen hast.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -738,4 +738,46 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get galleryDetails => 'Show details';
|
String get galleryDetails => 'Show details';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsResetTutorials => 'Show tutorials again';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsResetTutorialsDesc =>
|
||||||
|
'Click here to show already displayed tutorials again.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsResetTutorialsSuccess =>
|
||||||
|
'Tutorials will be displayed again.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatListSearchUsersTitle =>
|
||||||
|
'Find Friends and Manage Friend Requests';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatListSearchUsersDesc =>
|
||||||
|
'If you know your friends\' usernames, you can search for them here and send a friend request. You will also see all requests from other users that you can accept or block.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatListContextMenuTitle =>
|
||||||
|
'Long press on the contact to open the context menu.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatListContextMenuDesc =>
|
||||||
|
'With the context menu, you can pin, archive, and perform various actions on your contacts. Simply long press the contact and then move your finger to the desired option or tap directly on it.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatMessagesVerifyShieldTitle => 'Verify your contacts!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatMessagesVerifyShieldDesc =>
|
||||||
|
'twonly uses the Signal protocol for secure end-to-end encryption. When you first contact someone, their public identity key is downloaded. To ensure that this key has not been tampered with by third parties, you should compare it with your friend when you meet in person. Once you have verified the user, you can also enable the twonly mode when sending images and videos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatMessagesReopenMessageTitle =>
|
||||||
|
'Reopen Images and Videos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tutorialChatMessagesReopenMessageDesc =>
|
||||||
|
'If your friend has sent you a picture or video with infinite display time, you can open it again at any time until you restart the app. To do this, simply double-click on the message. Your friend will then receive a notification that you have viewed the picture again.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ class UserData {
|
||||||
|
|
||||||
DateTime? lastImageSend;
|
DateTime? lastImageSend;
|
||||||
int? todaysImageCounter;
|
int? todaysImageCounter;
|
||||||
|
List<String>? tutorialDisplayed;
|
||||||
|
|
||||||
int? myBestFriendContactId;
|
int? myBestFriendContactId;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['lastImageSend'] as String)
|
: DateTime.parse(json['lastImageSend'] as String)
|
||||||
..todaysImageCounter = (json['todaysImageCounter'] as num?)?.toInt()
|
..todaysImageCounter = (json['todaysImageCounter'] as num?)?.toInt()
|
||||||
|
..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList()
|
||||||
..myBestFriendContactId = (json['myBestFriendContactId'] as num?)?.toInt()
|
..myBestFriendContactId = (json['myBestFriendContactId'] as num?)?.toInt()
|
||||||
..signalLastSignedPreKeyUpdated =
|
..signalLastSignedPreKeyUpdated =
|
||||||
json['signalLastSignedPreKeyUpdated'] == null
|
json['signalLastSignedPreKeyUpdated'] == null
|
||||||
|
|
@ -60,6 +63,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||||
'additionalUserInvites': instance.additionalUserInvites,
|
'additionalUserInvites': instance.additionalUserInvites,
|
||||||
'lastImageSend': instance.lastImageSend?.toIso8601String(),
|
'lastImageSend': instance.lastImageSend?.toIso8601String(),
|
||||||
'todaysImageCounter': instance.todaysImageCounter,
|
'todaysImageCounter': instance.todaysImageCounter,
|
||||||
|
'tutorialDisplayed': instance.tutorialDisplayed,
|
||||||
'myBestFriendContactId': instance.myBestFriendContactId,
|
'myBestFriendContactId': instance.myBestFriendContactId,
|
||||||
'signalLastSignedPreKeyUpdated':
|
'signalLastSignedPreKeyUpdated':
|
||||||
instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
|
instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import 'package:twonly/src/views/settings/settings_main.view.dart';
|
||||||
import 'package:twonly/src/views/chats/add_new_user.view.dart';
|
import 'package:twonly/src/views/chats/add_new_user.view.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
|
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
|
||||||
|
import 'package:twonly/src/views/tutorial/tutorials.dart';
|
||||||
|
|
||||||
class ChatListView extends StatefulWidget {
|
class ChatListView extends StatefulWidget {
|
||||||
const ChatListView({super.key});
|
const ChatListView({super.key});
|
||||||
|
|
@ -30,6 +31,45 @@ class ChatListView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatListViewState extends State<ChatListView> {
|
class _ChatListViewState extends State<ChatListView> {
|
||||||
|
late StreamSubscription<List<Contact>> _contactsSub;
|
||||||
|
List<Contact> _contacts = [];
|
||||||
|
List<Contact> _pinnedContacts = [];
|
||||||
|
|
||||||
|
GlobalKey firstUserListItemKey = GlobalKey();
|
||||||
|
GlobalKey searchForOtherUsers = GlobalKey();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
initAsync();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future initAsync() async {
|
||||||
|
final stream = twonlyDB.contactsDao.watchContactsForChatList();
|
||||||
|
_contactsSub = stream.listen((contacts) {
|
||||||
|
setState(() {
|
||||||
|
_contacts = contacts.where((x) => !x.pinned).toList();
|
||||||
|
_pinnedContacts = contacts.where((x) => x.pinned).toList();
|
||||||
|
});
|
||||||
|
;
|
||||||
|
});
|
||||||
|
|
||||||
|
Future.delayed(Duration(seconds: 1), () async {
|
||||||
|
if (!mounted) return;
|
||||||
|
await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_contacts.isNotEmpty) {
|
||||||
|
await showChatListTutorialContextMenu(context, firstUserListItemKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_contactsSub.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool isConnected = context.watch<CustomChangeProvider>().isConnected;
|
bool isConnected = context.watch<CustomChangeProvider>().isConnected;
|
||||||
|
|
@ -71,6 +111,7 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
return NotificationBadge(
|
return NotificationBadge(
|
||||||
count: count.toString(),
|
count: count.toString(),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
|
key: searchForOtherUsers,
|
||||||
icon: FaIcon(FontAwesomeIcons.userPlus, size: 18),
|
icon: FaIcon(FontAwesomeIcons.userPlus, size: 18),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
|
|
@ -106,16 +147,8 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
child: isConnected ? Container() : ConnectionInfo(),
|
child: isConnected ? Container() : ConnectionInfo(),
|
||||||
),
|
),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: StreamBuilder(
|
child: (_contacts.isEmpty && _pinnedContacts.isEmpty)
|
||||||
stream: twonlyDB.contactsDao.watchContactsForChatList(),
|
? Center(
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData || snapshot.data == null) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
var contacts = snapshot.data!;
|
|
||||||
if (contacts.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
|
|
@ -131,59 +164,57 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
label:
|
label:
|
||||||
Text(context.lang.chatListViewSearchUserNameBtn)),
|
Text(context.lang.chatListViewSearchUserNameBtn)),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}
|
: RefreshIndicator(
|
||||||
|
|
||||||
final pinnedUsers = contacts.where((c) => c.pinned).toList();
|
|
||||||
|
|
||||||
return RefreshIndicator(
|
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await apiService.close(() {});
|
await apiService.close(() {});
|
||||||
await apiService.connect();
|
await apiService.connect();
|
||||||
await Future.delayed(Duration(seconds: 1));
|
await Future.delayed(Duration(seconds: 1));
|
||||||
},
|
},
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: pinnedUsers.length +
|
itemCount: _pinnedContacts.length +
|
||||||
(pinnedUsers.isNotEmpty ? 1 : 0) +
|
(_pinnedContacts.isNotEmpty ? 1 : 0) +
|
||||||
contacts.where((c) => !c.pinned).length,
|
_contacts.length,
|
||||||
itemExtentBuilder: (index, dimensions) {
|
itemExtentBuilder: (index, dimensions) {
|
||||||
int adjustedIndex = index - pinnedUsers.length;
|
int adjustedIndex = index - _pinnedContacts.length;
|
||||||
if (pinnedUsers.isNotEmpty && adjustedIndex == 0) {
|
if (_pinnedContacts.isNotEmpty && adjustedIndex == 0) {
|
||||||
return 16;
|
return 16;
|
||||||
}
|
}
|
||||||
return 72;
|
return 72;
|
||||||
},
|
},
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
// Check if the index is for the pinned users
|
// Check if the index is for the pinned users
|
||||||
if (index < pinnedUsers.length) {
|
if (index < _pinnedContacts.length) {
|
||||||
final contact = pinnedUsers[index];
|
final contact = _pinnedContacts[index];
|
||||||
return UserListItem(
|
return UserListItem(
|
||||||
key: ValueKey(contact.userId),
|
key: ValueKey(contact.userId),
|
||||||
user: contact,
|
user: contact,
|
||||||
|
firstUserListItemKey:
|
||||||
|
(index == 0) ? firstUserListItemKey : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are pinned users, account for the Divider
|
// If there are pinned users, account for the Divider
|
||||||
int adjustedIndex = index - pinnedUsers.length;
|
int adjustedIndex = index - _pinnedContacts.length;
|
||||||
if (pinnedUsers.isNotEmpty && adjustedIndex == 0) {
|
if (_pinnedContacts.isNotEmpty && adjustedIndex == 0) {
|
||||||
return Divider();
|
return Divider();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust the index for the contacts list
|
// Adjust the index for the contacts list
|
||||||
adjustedIndex -= (pinnedUsers.isNotEmpty ? 1 : 0);
|
adjustedIndex -= (_pinnedContacts.isNotEmpty ? 1 : 0);
|
||||||
|
|
||||||
// Get the contacts that are not pinned
|
// Get the contacts that are not pinned
|
||||||
final contact = contacts
|
final contact = _contacts.elementAt(
|
||||||
.where((c) => !c.pinned)
|
adjustedIndex,
|
||||||
.elementAt(adjustedIndex);
|
);
|
||||||
return UserListItem(
|
return UserListItem(
|
||||||
key: ValueKey(contact.userId),
|
key: ValueKey(contact.userId),
|
||||||
user: contact,
|
user: contact,
|
||||||
|
firstUserListItemKey:
|
||||||
|
(index == 0) ? firstUserListItemKey : null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -209,11 +240,10 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
|
|
||||||
class UserListItem extends StatefulWidget {
|
class UserListItem extends StatefulWidget {
|
||||||
final Contact user;
|
final Contact user;
|
||||||
|
final GlobalKey? firstUserListItemKey;
|
||||||
|
|
||||||
const UserListItem({
|
const UserListItem(
|
||||||
super.key,
|
{super.key, required this.user, required this.firstUserListItemKey});
|
||||||
required this.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<UserListItem> createState() => _UserListItem();
|
State<UserListItem> createState() => _UserListItem();
|
||||||
|
|
@ -315,10 +345,24 @@ class _UserListItem extends State<UserListItem> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
int flameCounter = getFlameCounterFromContact(widget.user);
|
int flameCounter = getFlameCounterFromContact(widget.user);
|
||||||
|
|
||||||
return UserContextMenu(
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 50,
|
||||||
|
child: SizedBox(
|
||||||
|
key: widget.firstUserListItemKey,
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
UserContextMenu(
|
||||||
contact: widget.user,
|
contact: widget.user,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(getContactDisplayName(widget.user)),
|
title: Text(
|
||||||
|
getContactDisplayName(widget.user),
|
||||||
|
),
|
||||||
subtitle: (currentMessage == null)
|
subtitle: (currentMessage == null)
|
||||||
? Text(context.lang.chatsTapToSend)
|
? Text(context.lang.chatsTapToSend)
|
||||||
: Row(
|
: Row(
|
||||||
|
|
@ -388,6 +432,8 @@ class _UserListItem extends State<UserListItem> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/views/contact/contact.view.dart';
|
import 'package:twonly/src/views/contact/contact.view.dart';
|
||||||
import 'package:twonly/src/model/memory_item.model.dart';
|
import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
|
import 'package:twonly/src/views/tutorial/tutorials.dart';
|
||||||
|
|
||||||
Color getMessageColor(Message message) {
|
Color getMessageColor(Message message) {
|
||||||
return (message.messageOtherId == null)
|
return (message.messageOtherId == null)
|
||||||
|
|
@ -48,6 +49,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
Map<int, List<Message>> textReactionsToMessageId = {};
|
Map<int, List<Message>> textReactionsToMessageId = {};
|
||||||
Map<int, List<Message>> emojiReactionsToMessageId = {};
|
Map<int, List<Message>> emojiReactionsToMessageId = {};
|
||||||
Message? responseToMessage;
|
Message? responseToMessage;
|
||||||
|
GlobalKey verifyShieldKey = GlobalKey();
|
||||||
late FocusNode textFieldFocus;
|
late FocusNode textFieldFocus;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -56,6 +58,11 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
user = widget.contact;
|
user = widget.contact;
|
||||||
textFieldFocus = FocusNode();
|
textFieldFocus = FocusNode();
|
||||||
initStreams();
|
initStreams();
|
||||||
|
|
||||||
|
Future.delayed(Duration(seconds: 1), () async {
|
||||||
|
if (!mounted) return;
|
||||||
|
await showVerifyShieldTutorial(context, verifyShieldKey);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -251,7 +258,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
children: [
|
children: [
|
||||||
Text(getContactDisplayName(user)),
|
Text(getContactDisplayName(user)),
|
||||||
SizedBox(width: 10),
|
SizedBox(width: 10),
|
||||||
VerifiedShield(user),
|
VerifiedShield(key: verifyShieldKey, user),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@ import 'package:twonly/src/services/api/media_received.dart' as received;
|
||||||
import 'package:twonly/src/services/notification.service.dart';
|
import 'package:twonly/src/services/notification.service.dart';
|
||||||
import 'package:twonly/src/views/chats/media_viewer.view.dart';
|
import 'package:twonly/src/views/chats/media_viewer.view.dart';
|
||||||
import 'package:twonly/src/model/memory_item.model.dart';
|
import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
|
import 'package:twonly/src/views/tutorial/tutorials.dart';
|
||||||
|
|
||||||
class ChatMediaEntry extends StatelessWidget {
|
class ChatMediaEntry extends StatefulWidget {
|
||||||
const ChatMediaEntry({
|
const ChatMediaEntry({
|
||||||
super.key,
|
super.key,
|
||||||
required this.message,
|
required this.message,
|
||||||
|
|
@ -25,51 +26,82 @@ class ChatMediaEntry extends StatelessWidget {
|
||||||
final MessageContent content;
|
final MessageContent content;
|
||||||
final List<MemoryItem> galleryItems;
|
final List<MemoryItem> galleryItems;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChatMediaEntry> createState() => _ChatMediaEntryState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
|
GlobalKey reopenMediaFile = GlobalKey();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
checkIfTutorialCanBeShown();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future checkIfTutorialCanBeShown() async {
|
||||||
|
if (widget.message.openedAt == null &&
|
||||||
|
widget.message.messageOtherId != null ||
|
||||||
|
widget.message.mediaStored) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (await received.existsMediaFile(widget.message.messageId, "png")) {
|
||||||
|
Future.delayed(Duration(seconds: 1), () {
|
||||||
|
if (!mounted) return;
|
||||||
|
showReopenMediaFilesTutorial(context, reopenMediaFile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Color color = getMessageColorFromType(
|
Color color = getMessageColorFromType(
|
||||||
content,
|
widget.content,
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
key: reopenMediaFile,
|
||||||
onDoubleTap: () async {
|
onDoubleTap: () async {
|
||||||
if (message.openedAt == null && message.messageOtherId != null ||
|
if (widget.message.openedAt == null &&
|
||||||
message.mediaStored) {
|
widget.message.messageOtherId != null ||
|
||||||
|
widget.message.mediaStored) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (await received.existsMediaFile(message.messageId, "png")) {
|
if (await received.existsMediaFile(widget.message.messageId, "png")) {
|
||||||
await encryptAndSendMessageAsync(
|
await encryptAndSendMessageAsync(
|
||||||
null,
|
null,
|
||||||
contact.userId,
|
widget.contact.userId,
|
||||||
MessageJson(
|
MessageJson(
|
||||||
kind: MessageKind.reopenedMedia,
|
kind: MessageKind.reopenedMedia,
|
||||||
messageId: message.messageId,
|
messageId: widget.message.messageId,
|
||||||
content: ReopenedMediaFileContent(
|
content: ReopenedMediaFileContent(
|
||||||
messageId: message.messageOtherId!,
|
messageId: widget.message.messageOtherId!,
|
||||||
),
|
),
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
),
|
),
|
||||||
pushKind: PushKind.reopenedMedia,
|
pushKind: PushKind.reopenedMedia,
|
||||||
);
|
);
|
||||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||||
message.messageId,
|
widget.message.messageId,
|
||||||
MessagesCompanion(openedAt: Value(null)),
|
MessagesCompanion(openedAt: Value(null)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
if (message.kind == MessageKind.media) {
|
if (widget.message.kind == MessageKind.media) {
|
||||||
if (message.downloadState == DownloadState.downloaded &&
|
if (widget.message.downloadState == DownloadState.downloaded &&
|
||||||
message.openedAt == null) {
|
widget.message.openedAt == null) {
|
||||||
Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) {
|
MaterialPageRoute(builder: (context) {
|
||||||
return MediaViewerView(contact, initialMessage: message);
|
return MediaViewerView(widget.contact,
|
||||||
|
initialMessage: widget.message);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else if (message.downloadState == DownloadState.pending) {
|
checkIfTutorialCanBeShown();
|
||||||
received.startDownloadMedia(message, true);
|
} else if (widget.message.downloadState == DownloadState.pending) {
|
||||||
|
received.startDownloadMedia(widget.message, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -80,10 +112,10 @@ class ChatMediaEntry extends StatelessWidget {
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: InChatMediaViewer(
|
child: InChatMediaViewer(
|
||||||
message: message,
|
message: widget.message,
|
||||||
contact: contact,
|
contact: widget.contact,
|
||||||
color: color,
|
color: color,
|
||||||
galleryItems: galleryItems,
|
galleryItems: widget.galleryItems,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
import 'package:twonly/src/views/settings/help/contact_us.view.dart';
|
import 'package:twonly/src/views/settings/help/contact_us.view.dart';
|
||||||
import 'package:twonly/src/views/settings/help/credits.view.dart';
|
import 'package:twonly/src/views/settings/help/credits.view.dart';
|
||||||
import 'package:twonly/src/views/settings/help/diagnostics.view.dart';
|
import 'package:twonly/src/views/settings/help/diagnostics.view.dart';
|
||||||
|
|
@ -34,6 +35,23 @@ class HelpView extends StatelessWidget {
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.lang.settingsResetTutorials),
|
||||||
|
subtitle: Text(context.lang.settingsResetTutorialsDesc),
|
||||||
|
onTap: () async {
|
||||||
|
final user = await getUser();
|
||||||
|
if (user == null) return;
|
||||||
|
user.tutorialDisplayed = [];
|
||||||
|
await updateUser(user);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.lang.settingsResetTutorialsSuccess),
|
||||||
|
duration: Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
Divider(),
|
Divider(),
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: PackageInfo.fromPlatform(),
|
future: PackageInfo.fromPlatform(),
|
||||||
|
|
|
||||||
116
lib/src/views/tutorial/show_tutorial.dart
Normal file
116
lib/src/views/tutorial/show_tutorial.dart
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:mutex/mutex.dart';
|
||||||
|
import 'package:tutorial_coach_mark/tutorial_coach_mark.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
|
||||||
|
final lockDisplayTutorial = Mutex();
|
||||||
|
|
||||||
|
Future showTutorial(BuildContext context, List<TargetFocus> targets) async {
|
||||||
|
await lockDisplayTutorial.protect(() async {
|
||||||
|
Completer completer = Completer();
|
||||||
|
TutorialCoachMark(
|
||||||
|
targets: targets,
|
||||||
|
colorShadow: context.color.primary,
|
||||||
|
textSkip: context.lang.ok,
|
||||||
|
alignSkip: Alignment.bottomCenter,
|
||||||
|
textStyleSkip: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
onClickTarget: (target) {
|
||||||
|
print(target);
|
||||||
|
},
|
||||||
|
onClickTargetWithTapPosition: (target, tapDetails) {
|
||||||
|
print("target: $target");
|
||||||
|
print(
|
||||||
|
"clicked at position local: ${tapDetails.localPosition} - global: ${tapDetails.globalPosition}");
|
||||||
|
},
|
||||||
|
onClickOverlay: (target) {
|
||||||
|
print(target);
|
||||||
|
},
|
||||||
|
onSkip: () {
|
||||||
|
completer.complete();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onFinish: () {
|
||||||
|
completer.complete();
|
||||||
|
},
|
||||||
|
).show(context: context);
|
||||||
|
|
||||||
|
await completer.future;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> checkIfTutorialAlreadyShown(String tutorialId) async {
|
||||||
|
final user = await getUser();
|
||||||
|
if (user == null) return true;
|
||||||
|
user.tutorialDisplayed ??= [];
|
||||||
|
if (user.tutorialDisplayed!.contains(tutorialId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
user.tutorialDisplayed!.add(tutorialId);
|
||||||
|
await updateUser(user);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TargetFocus getTargetFocus(
|
||||||
|
BuildContext context, GlobalKey key, String title, String body) {
|
||||||
|
RenderBox renderBox = key.currentContext?.findRenderObject() as RenderBox;
|
||||||
|
Offset position = renderBox.localToGlobal(Offset.zero);
|
||||||
|
double screenHeight = MediaQuery.of(context).size.height;
|
||||||
|
double centerY = screenHeight / 2;
|
||||||
|
|
||||||
|
double top = 0;
|
||||||
|
double bottom = 0;
|
||||||
|
|
||||||
|
if (position.dy < centerY) {
|
||||||
|
bottom = 0;
|
||||||
|
top = position.dy;
|
||||||
|
} else {
|
||||||
|
bottom = centerY;
|
||||||
|
top = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TargetFocus(
|
||||||
|
identify: title,
|
||||||
|
keyTarget: key,
|
||||||
|
contents: [
|
||||||
|
TargetContent(
|
||||||
|
align: ContentAlign.custom,
|
||||||
|
customPosition: CustomTargetContentPosition(
|
||||||
|
left: 0, right: 0, top: top, bottom: bottom),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 20.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0),
|
||||||
|
child: Text(
|
||||||
|
body,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
76
lib/src/views/tutorial/tutorials.dart
Normal file
76
lib/src/views/tutorial/tutorials.dart
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tutorial_coach_mark/tutorial_coach_mark.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/views/tutorial/show_tutorial.dart';
|
||||||
|
|
||||||
|
Future showChatListTutorialSearchOtherUsers(
|
||||||
|
BuildContext context,
|
||||||
|
GlobalKey searchForOtherUsers,
|
||||||
|
) async {
|
||||||
|
if (await checkIfTutorialAlreadyShown("chat_list:search_users")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
List<TargetFocus> targets = [];
|
||||||
|
targets.add(getTargetFocus(
|
||||||
|
context,
|
||||||
|
searchForOtherUsers,
|
||||||
|
context.lang.tutorialChatListSearchUsersTitle,
|
||||||
|
context.lang.tutorialChatListSearchUsersDesc,
|
||||||
|
));
|
||||||
|
await showTutorial(context, targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future showChatListTutorialContextMenu(
|
||||||
|
BuildContext context,
|
||||||
|
GlobalKey firstUserListItemKey,
|
||||||
|
) async {
|
||||||
|
if (await checkIfTutorialAlreadyShown("chat_list:context_menu")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
List<TargetFocus> targets = [];
|
||||||
|
targets.add(getTargetFocus(
|
||||||
|
context,
|
||||||
|
firstUserListItemKey,
|
||||||
|
context.lang.tutorialChatListContextMenuTitle,
|
||||||
|
context.lang.tutorialChatListContextMenuDesc,
|
||||||
|
));
|
||||||
|
await showTutorial(context, targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future showVerifyShieldTutorial(
|
||||||
|
BuildContext context,
|
||||||
|
GlobalKey firstUserListItemKey,
|
||||||
|
) async {
|
||||||
|
if (await checkIfTutorialAlreadyShown("chat_messages:verify_shield")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
List<TargetFocus> targets = [];
|
||||||
|
targets.add(getTargetFocus(
|
||||||
|
context,
|
||||||
|
firstUserListItemKey,
|
||||||
|
context.lang.tutorialChatMessagesVerifyShieldTitle,
|
||||||
|
context.lang.tutorialChatMessagesVerifyShieldDesc,
|
||||||
|
));
|
||||||
|
await showTutorial(context, targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future showReopenMediaFilesTutorial(
|
||||||
|
BuildContext context,
|
||||||
|
GlobalKey firstUserListItemKey,
|
||||||
|
) async {
|
||||||
|
if (await checkIfTutorialAlreadyShown("chat_messages:reopen_message")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
List<TargetFocus> targets = [];
|
||||||
|
targets.add(getTargetFocus(
|
||||||
|
context,
|
||||||
|
firstUserListItemKey,
|
||||||
|
context.lang.tutorialChatMessagesReopenMessageTitle,
|
||||||
|
context.lang.tutorialChatMessagesReopenMessageDesc,
|
||||||
|
));
|
||||||
|
await showTutorial(context, targets);
|
||||||
|
}
|
||||||
24
pubspec.lock
24
pubspec.lock
|
|
@ -1347,22 +1347,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.5.0"
|
||||||
qr:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: qr
|
|
||||||
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.2"
|
|
||||||
qr_flutter:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: qr_flutter
|
|
||||||
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.1.0"
|
|
||||||
recase:
|
recase:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1656,6 +1640,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.2"
|
||||||
|
tutorial_coach_mark:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: tutorial_coach_mark
|
||||||
|
sha256: "9cdb721165d1cfb6e9b1910a1af1b3570fa6caa5059cf1506fcbd00bf7102abf"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ dependencies:
|
||||||
video_compress: ^3.1.4
|
video_compress: ^3.1.4
|
||||||
share_plus: ^11.0.0
|
share_plus: ^11.0.0
|
||||||
photo_view: ^0.15.0
|
photo_view: ^0.15.0
|
||||||
|
tutorial_coach_mark: ^1.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue