diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2152468..e2b4147 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+
+## 0.0.93
+
+- Feature: Verification checkmark for friends
+- Fix: Added contacts in contact sharing that were not clickable.
+- Fix: Open chat after the image expires in case a draft message exists
+- Fix: Restore flames as a plus user
+- Fix: Route not found when sharing image
+- Fix: Increase recent limit in emoji keyboard
+- Fix: Increase show time of the focus indication
+- Fix: Quoted text message not shown properly
+- Fix: Push notification in groups when someone saves an image
+- Fix: Dark mode in diagnostics view
+
## 0.0.92
- Adds the option to share contacts
diff --git a/analysis_options.yaml b/analysis_options.yaml
index fc69522..4328836 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -11,6 +11,7 @@ analyzer:
avoid_positional_boolean_parameters: ignore
inference_failure_on_collection_literal: ignore
matching_super_parameters: ignore
+ parameter_assignments: ignore
exclude:
- "lib/src/model/protobuf/**"
- "lib/src/model/protobuf/api/websocket/**"
diff --git a/assets/icons/verified_badge_green.svg b/assets/icons/verified_badge_green.svg
new file mode 100644
index 0000000..98c69ac
--- /dev/null
+++ b/assets/icons/verified_badge_green.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/assets/icons/verified_badge_red.svg b/assets/icons/verified_badge_red.svg
new file mode 100644
index 0000000..6e70a82
--- /dev/null
+++ b/assets/icons/verified_badge_red.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/assets/icons/verified_badge_yellow.svg b/assets/icons/verified_badge_yellow.svg
new file mode 100644
index 0000000..29ee80a
--- /dev/null
+++ b/assets/icons/verified_badge_yellow.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/lib/src/constants/routes.keys.dart b/lib/src/constants/routes.keys.dart
index aa8bab5..29ed8af 100644
--- a/lib/src/constants/routes.keys.dart
+++ b/lib/src/constants/routes.keys.dart
@@ -39,6 +39,8 @@ class Routes {
static const String settingsStorageExport = '/settings/storage_data/export';
static const String settingsHelp = '/settings/help';
static const String settingsHelpFaq = '/settings/help/faq';
+ static const String settingsHelpFaqVerifyBadge =
+ '/settings/help/faq/verifybadge';
static const String settingsHelpContactUs = '/settings/help/contact_us';
static const String settingsHelpDiagnostics = '/settings/help/diagnostics';
static const String settingsHelpUserStudy = '/settings/help/user_study';
diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart
index 1821ee6..934c86e 100644
--- a/lib/src/localization/generated/app_localizations.dart
+++ b/lib/src/localization/generated/app_localizations.dart
@@ -943,7 +943,7 @@ abstract class AppLocalizations {
/// No description provided for @contactVerifyNumberTitle.
///
/// In en, this message translates to:
- /// **'Verify safety number'**
+ /// **'Verify contact'**
String get contactVerifyNumberTitle;
/// No description provided for @contactVerifyNumberTapToScan.
@@ -970,6 +970,12 @@ abstract class AppLocalizations {
/// **'To verify the end-to-end encryption with {username}, compare the numbers with their device. The person can also scan your code with their device.'**
String contactVerifyNumberLongDesc(Object username);
+ /// No description provided for @contactViewMessage.
+ ///
+ /// In en, this message translates to:
+ /// **'Message'**
+ String get contactViewMessage;
+
/// No description provided for @contactNickname.
///
/// In en, this message translates to:
@@ -2991,6 +2997,42 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'To see this message, you need to update twonly.'**
String get updateTwonlyMessage;
+
+ /// No description provided for @verificationBadgeNote.
+ ///
+ /// In en, this message translates to:
+ /// **'You can verify your friends by scanning their public QR code. Click to learn more.'**
+ String get verificationBadgeNote;
+
+ /// No description provided for @verificationBadgeTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Verification'**
+ String get verificationBadgeTitle;
+
+ /// No description provided for @verificationBadgeGeneralDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'The green checkmark gives you the certainty that you are messaging the right person.'**
+ String get verificationBadgeGeneralDesc;
+
+ /// No description provided for @verificationBadgeGreenDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'Contact that you have personally verified via QR code. This also verified their public key.'**
+ String get verificationBadgeGreenDesc;
+
+ /// No description provided for @verificationBadgeYellowDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'(Coming soon) Contact whose QR code was scanned by one of your personally verified contacts.'**
+ String get verificationBadgeYellowDesc;
+
+ /// No description provided for @verificationBadgeRedDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'Unknown contact whose identity has not yet been verified.'**
+ String get verificationBadgeRedDesc;
}
class _AppLocalizationsDelegate
diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart
index 4385fee..1ad8949 100644
--- a/lib/src/localization/generated/app_localizations_de.dart
+++ b/lib/src/localization/generated/app_localizations_de.dart
@@ -468,7 +468,7 @@ class AppLocalizationsDe extends AppLocalizations {
'Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.';
@override
- String get contactVerifyNumberTitle => 'Sicherheitsnummer verifizieren';
+ String get contactVerifyNumberTitle => 'Benutzer verifizieren';
@override
String get contactVerifyNumberTapToScan => 'Zum Scannen tippen';
@@ -484,6 +484,9 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Um die Ende-zu-Ende-Verschlüsselung mit $username zu verifizieren, vergleiche die Zahlen mit ihrem Gerät. Die Person kann auch deinen Code mit ihrem Gerät scannen.';
}
+ @override
+ String get contactViewMessage => 'Nachricht';
+
@override
String get contactNickname => 'Spitzname';
@@ -1669,4 +1672,27 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get updateTwonlyMessage =>
'Um diese Nachricht zu sehen, musst du twonly aktualisieren.';
+
+ @override
+ String get verificationBadgeNote =>
+ 'Du kannst deine Freunde verifizieren, indem du deren öffentlichen QR-Code scannst. Klicke, um mehr zu erfahren.';
+
+ @override
+ String get verificationBadgeTitle => 'Verifizierung';
+
+ @override
+ String get verificationBadgeGeneralDesc =>
+ 'Der grüne Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst.';
+
+ @override
+ String get verificationBadgeGreenDesc =>
+ 'Kontakt, den du durch den QR-Code persönlich verifiziert hast. Dadurch wurde auch sein öffentlicher Schlüssel überprüft.';
+
+ @override
+ String get verificationBadgeYellowDesc =>
+ '(Coming soon) Kontakt, dessen QR-Code von einem deiner persönlich verifizierten Kontakte gescannt wurde.';
+
+ @override
+ String get verificationBadgeRedDesc =>
+ 'Unbekannter Kontakt, dessen Identität bisher nicht verifiziert wurde.';
}
diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart
index 62fee3d..3513748 100644
--- a/lib/src/localization/generated/app_localizations_en.dart
+++ b/lib/src/localization/generated/app_localizations_en.dart
@@ -463,7 +463,7 @@ class AppLocalizationsEn extends AppLocalizations {
'Your account will be deleted. There is no change to restore it.';
@override
- String get contactVerifyNumberTitle => 'Verify safety number';
+ String get contactVerifyNumberTitle => 'Verify contact';
@override
String get contactVerifyNumberTapToScan => 'Tap to scan';
@@ -479,6 +479,9 @@ class AppLocalizationsEn extends AppLocalizations {
return 'To verify the end-to-end encryption with $username, compare the numbers with their device. The person can also scan your code with their device.';
}
+ @override
+ String get contactViewMessage => 'Message';
+
@override
String get contactNickname => 'Nickname';
@@ -1657,4 +1660,27 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get updateTwonlyMessage =>
'To see this message, you need to update twonly.';
+
+ @override
+ String get verificationBadgeNote =>
+ 'You can verify your friends by scanning their public QR code. Click to learn more.';
+
+ @override
+ String get verificationBadgeTitle => 'Verification';
+
+ @override
+ String get verificationBadgeGeneralDesc =>
+ 'The green checkmark gives you the certainty that you are messaging the right person.';
+
+ @override
+ String get verificationBadgeGreenDesc =>
+ 'Contact that you have personally verified via QR code. This also verified their public key.';
+
+ @override
+ String get verificationBadgeYellowDesc =>
+ '(Coming soon) Contact whose QR code was scanned by one of your personally verified contacts.';
+
+ @override
+ String get verificationBadgeRedDesc =>
+ 'Unknown contact whose identity has not yet been verified.';
}
diff --git a/lib/src/localization/generated/app_localizations_sv.dart b/lib/src/localization/generated/app_localizations_sv.dart
index 286fb3b..a32c895 100644
--- a/lib/src/localization/generated/app_localizations_sv.dart
+++ b/lib/src/localization/generated/app_localizations_sv.dart
@@ -463,7 +463,7 @@ class AppLocalizationsSv extends AppLocalizations {
'Your account will be deleted. There is no change to restore it.';
@override
- String get contactVerifyNumberTitle => 'Verify safety number';
+ String get contactVerifyNumberTitle => 'Verify contact';
@override
String get contactVerifyNumberTapToScan => 'Tap to scan';
@@ -479,6 +479,9 @@ class AppLocalizationsSv extends AppLocalizations {
return 'To verify the end-to-end encryption with $username, compare the numbers with their device. The person can also scan your code with their device.';
}
+ @override
+ String get contactViewMessage => 'Message';
+
@override
String get contactNickname => 'Nickname';
@@ -1657,4 +1660,27 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get updateTwonlyMessage =>
'To see this message, you need to update twonly.';
+
+ @override
+ String get verificationBadgeNote =>
+ 'You can verify your friends by scanning their public QR code. Click to learn more.';
+
+ @override
+ String get verificationBadgeTitle => 'Verification';
+
+ @override
+ String get verificationBadgeGeneralDesc =>
+ 'The green checkmark gives you the certainty that you are messaging the right person.';
+
+ @override
+ String get verificationBadgeGreenDesc =>
+ 'Contact that you have personally verified via QR code. This also verified their public key.';
+
+ @override
+ String get verificationBadgeYellowDesc =>
+ '(Coming soon) Contact whose QR code was scanned by one of your personally verified contacts.';
+
+ @override
+ String get verificationBadgeRedDesc =>
+ 'Unknown contact whose identity has not yet been verified.';
}
diff --git a/lib/src/localization/translations b/lib/src/localization/translations
index 69d295d..6147155 160000
--- a/lib/src/localization/translations
+++ b/lib/src/localization/translations
@@ -1 +1 @@
-Subproject commit 69d295db737253e0c1b68aedc39bf757e8d642e6
+Subproject commit 6147155ce50caa97864d56e42e49a6f54702785d
diff --git a/lib/src/providers/purchases.provider.dart b/lib/src/providers/purchases.provider.dart
index 29ba6a8..3895c18 100644
--- a/lib/src/providers/purchases.provider.dart
+++ b/lib/src/providers/purchases.provider.dart
@@ -83,7 +83,7 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
if (user != null && isPayingUser(planFromString(user.subscriptionPlan))) {
Log.info('Started IPA timer for verification.');
globalForceIpaCheck = Timer(const Duration(seconds: 5), () async {
- Log.warn('Force Ipa check was not stopped. Requesting forced check...');
+ Log.info('Force Ipa check was not stopped. Requesting forced check...');
await apiService.forceIpaCheck();
});
}
diff --git a/lib/src/providers/routing.provider.dart b/lib/src/providers/routing.provider.dart
index 395a7fb..87f7c4b 100644
--- a/lib/src/providers/routing.provider.dart
+++ b/lib/src/providers/routing.provider.dart
@@ -32,6 +32,7 @@ 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/diagnostics.view.dart';
import 'package:twonly/src/views/settings/help/faq.view.dart';
+import 'package:twonly/src/views/settings/help/faq/verifybadge.dart';
import 'package:twonly/src/views/settings/help/help.view.dart';
import 'package:twonly/src/views/settings/notification.view.dart';
import 'package:twonly/src/views/settings/privacy.view.dart';
@@ -227,6 +228,12 @@ final routerProvider = GoRouter(
GoRoute(
path: 'faq',
builder: (context, state) => const FaqView(),
+ routes: [
+ GoRoute(
+ path: 'verifybadge',
+ builder: (context, state) => const VerificationBadeFaqView(),
+ ),
+ ],
),
GoRoute(
path: 'contact_us',
@@ -281,5 +288,10 @@ final routerProvider = GoRouter(
),
],
),
+ // Fallback instead of showing a Page Not Found error redirect to home
+ GoRoute(
+ path: '/:path(.*)',
+ redirect: (context, state) => '/',
+ ),
],
);
diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart
index 816f575..79c34b1 100644
--- a/lib/src/services/api.service.dart
+++ b/lib/src/services/api.service.dart
@@ -193,7 +193,11 @@ class ApiService {
}
Future _onError(dynamic e) async {
- Log.warn('websocket error: $e');
+ if (e.toString().contains('Failed host lookup')) {
+ Log.info('WebSocket connection failed: Host not reachable.');
+ } else {
+ Log.warn('websocket error: $e');
+ }
await onClosed();
}
diff --git a/lib/src/services/api/mediafiles/download.service.dart b/lib/src/services/api/mediafiles/download.service.dart
index 6782e3b..ef8d17a 100644
--- a/lib/src/services/api/mediafiles/download.service.dart
+++ b/lib/src/services/api/mediafiles/download.service.dart
@@ -37,6 +37,14 @@ Future canMediaFileBeDownloaded(MediaFile mediaFile) async {
// If not delete the message as it can not be downloaded from the server anymore.
if (messages.length != 1) {
+ if (messages.isEmpty) {
+ MediaFileService(mediaFile).fullMediaRemoval();
+ await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
+ Log.warn(
+ 'Media file which is in downloading status has not text message. Deleting media file. ${mediaFile.mediaId}.',
+ );
+ return false;
+ }
Log.warn(
'A media for download must have one original message, but it has ${messages.length}.',
);
diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart
index dd83859..b4452eb 100644
--- a/lib/src/services/api/messages.dart
+++ b/lib/src/services/api/messages.dart
@@ -69,12 +69,14 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
try {
if (receiptId == null && receipt == null) return null;
if (receipt == null) {
+ // ignore: parameter_assignments
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!);
if (receipt == null) {
Log.warn('Receipt not found.');
return null;
}
}
+ // ignore: parameter_assignments
receiptId = receipt.receiptId;
final contact =
diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart
index dd319b8..2df0f34 100644
--- a/lib/src/services/notifications/pushkeys.notifications.dart
+++ b/lib/src/services/notifications/pushkeys.notifications.dart
@@ -281,6 +281,13 @@ Future getPushNotificationFromEncryptedContent(
}
if (content.hasMediaUpdate()) {
+ final msg = await twonlyDB.messagesDao
+ .getMessageById(content.reaction.targetMessageId)
+ .getSingleOrNull();
+ // These notifications should only be send to the original sender.
+ if (msg == null || msg.senderId == null || msg.senderId != toUserId) {
+ return null;
+ }
switch (content.mediaUpdate.type) {
case EncryptedContent_MediaUpdate_Type.REOPENED:
kind = PushKind.reopenedMedia;
diff --git a/lib/src/services/subscription.service.dart b/lib/src/services/subscription.service.dart
index c36797a..27b50cc 100644
--- a/lib/src/services/subscription.service.dart
+++ b/lib/src/services/subscription.service.dart
@@ -10,8 +10,20 @@ enum SubscriptionPlan {
Plus,
}
-bool isAdditionalAccount(SubscriptionPlan plan) {
- return plan == SubscriptionPlan.Free || plan == SubscriptionPlan.Plus;
+enum PremiumFeatures { RestoreFlames }
+
+const Map> planPermissions = {
+ PremiumFeatures.RestoreFlames: [
+ SubscriptionPlan.Family,
+ SubscriptionPlan.Plus,
+ SubscriptionPlan.Tester,
+ SubscriptionPlan.Pro,
+ ],
+};
+
+bool isUserAllowed(SubscriptionPlan plan, PremiumFeatures feature) {
+ final allowedPlans = planPermissions[feature] ?? [];
+ return allowedPlans.contains(plan);
}
bool isPayingUser(SubscriptionPlan plan) {
diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart
index e398fac..e7350fb 100644
--- a/lib/src/utils/misc.dart
+++ b/lib/src/utils/misc.dart
@@ -241,7 +241,7 @@ InputDecoration inputTextMessageDeco(BuildContext context) {
String truncateString(String input, {int maxLength = 20}) {
if (input.length > maxLength) {
- return '${input.substring(0, maxLength)}...';
+ return '${input.characters.take(maxLength)}...';
}
return input;
}
diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart
index da13414..a514b75 100644
--- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart
+++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart
@@ -207,6 +207,9 @@ class MainCameraController {
Log.error(e);
}
+ // display the focus point at least 500ms
+ await Future.delayed(const Duration(milliseconds: 500));
+
focusPointOffset = null;
setState();
}
diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart
index f025012..debc8f0 100644
--- a/lib/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart
+++ b/lib/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart
@@ -3,7 +3,9 @@ import 'dart:convert';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
+import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart';
+import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
import 'package:twonly/src/services/api/utils.dart';
@@ -99,7 +101,15 @@ class _ContactRow extends StatefulWidget {
class _ContactRowState extends State<_ContactRow> {
bool _isLoading = false;
- Future _onContactClick() async {
+ Future _onContactClick(bool isAdded) async {
+ if (widget.contact.userId.toInt() == gUser.userId) {
+ await context.push(Routes.settingsProfile);
+ return;
+ }
+ if (isAdded) {
+ await context.push(Routes.profileContact(widget.contact.userId.toInt()));
+ return;
+ }
setState(() {
_isLoading = true;
});
@@ -152,46 +162,48 @@ class _ContactRowState extends State<_ContactRow> {
widget.contact.userId.toInt() == gUser.userId;
return GestureDetector(
- onTap: (widget.message.senderId == null || isAdded || _isLoading)
- ? null
- : _onContactClick,
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- const FaIcon(
- FontAwesomeIcons.user,
- color: Colors.white,
- size: 16,
- ),
- const SizedBox(width: 8),
- Flexible(
- child: BetterText(
- text: widget.contact.displayName,
- textColor: Colors.white,
+ onTap: _isLoading ? null : () => _onContactClick(isAdded),
+ child: ColoredBox(
+ color: Colors.transparent,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const FaIcon(
+ FontAwesomeIcons.user,
+ color: Colors.white,
+ size: 16,
),
- ),
- if (widget.message.senderId != null && !isAdded) ...[
- const Spacer(),
const SizedBox(width: 8),
- if (_isLoading)
- const SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(
- strokeWidth: 2,
- valueColor: AlwaysStoppedAnimation(Colors.white),
- ),
- )
- else
- const FaIcon(
- FontAwesomeIcons.userPlus,
- color: Colors.white,
- size: 16,
+ Flexible(
+ child: BetterText(
+ text: widget.contact.displayName,
+ textColor: Colors.white,
),
+ ),
+ if (widget.message.senderId != null && !isAdded) ...[
+ const Spacer(),
+ const SizedBox(width: 8),
+ if (_isLoading)
+ const SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor:
+ AlwaysStoppedAnimation(Colors.white),
+ ),
+ )
+ else
+ const FaIcon(
+ FontAwesomeIcons.userPlus,
+ color: Colors.white,
+ size: 16,
+ ),
+ ],
],
- ],
+ ),
),
),
);
diff --git a/lib/src/views/chats/chat_messages_components/message_input.dart b/lib/src/views/chats/chat_messages_components/message_input.dart
index af683a2..1f5aa53 100644
--- a/lib/src/views/chats/chat_messages_components/message_input.dart
+++ b/lib/src/views/chats/chat_messages_components/message_input.dart
@@ -509,6 +509,7 @@ class _MessageInputState extends State {
TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)),
emojiViewConfig: EmojiViewConfig(
backgroundColor: context.color.surfaceContainer,
+ recentsLimit: 40,
),
searchViewConfig: SearchViewConfig(
backgroundColor: context.color.surfaceContainer,
diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart
index 43642af..ba1842f 100644
--- a/lib/src/views/chats/media_viewer.view.dart
+++ b/lib/src/views/chats/media_viewer.view.dart
@@ -4,10 +4,12 @@ import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
+import 'package:go_router/go_router.dart';
import 'package:lottie/lottie.dart';
import 'package:no_screenshot/no_screenshot.dart';
import 'package:photo_view/photo_view.dart';
import 'package:twonly/globals.dart';
+import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'
show DownloadState, MediaType;
@@ -131,7 +133,6 @@ class _MediaViewerViewState extends State {
}
setState(() {});
if (firstRun) {
- // ignore: parameter_assignments
firstRun = false;
await loadCurrentMediaFile();
}
@@ -154,7 +155,16 @@ class _MediaViewerViewState extends State {
progressTimer?.cancel();
if (allMediaFiles.isEmpty) {
- Navigator.pop(context);
+ final group = await twonlyDB.groupsDao.getGroup(widget.group.groupId);
+ if (mounted) {
+ if (group != null &&
+ group.draftMessage != null &&
+ group.draftMessage != '') {
+ context.replace(Routes.chatsMessages, extra: group);
+ } else {
+ Navigator.pop(context);
+ }
+ }
} else {
await loadCurrentMediaFile();
}
diff --git a/lib/src/views/chats/start_new_chat.view.dart b/lib/src/views/chats/start_new_chat.view.dart
index 09926fc..65d36b7 100644
--- a/lib/src/views/chats/start_new_chat.view.dart
+++ b/lib/src/views/chats/start_new_chat.view.dart
@@ -1,3 +1,5 @@
+// ignore_for_file: parameter_assignments
+
import 'dart:async';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
diff --git a/lib/src/views/components/better_list_title.dart b/lib/src/views/components/better_list_title.dart
index fe408a5..fac2f5e 100644
--- a/lib/src/views/components/better_list_title.dart
+++ b/lib/src/views/components/better_list_title.dart
@@ -27,20 +27,17 @@ class BetterListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
- leading: Padding(
- padding: (padding == null)
- ? const EdgeInsets.only(
- right: 10,
- left: 19,
- )
- : padding!,
- child: (leading != null)
- ? leading
- : FaIcon(
- icon,
- size: iconSize,
- color: color,
- ),
+ leading: SizedBox(
+ width: 50,
+ child: Center(
+ child: (leading != null)
+ ? leading
+ : FaIcon(
+ icon,
+ size: iconSize,
+ color: color,
+ ),
+ ),
),
trailing: trailing,
title: Text(
diff --git a/lib/src/views/components/emoji_picker.bottom.dart b/lib/src/views/components/emoji_picker.bottom.dart
index 6139966..2dff473 100755
--- a/lib/src/views/components/emoji_picker.bottom.dart
+++ b/lib/src/views/components/emoji_picker.bottom.dart
@@ -57,6 +57,7 @@ class EmojiPickerBottom extends StatelessWidget {
TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)),
emojiViewConfig: EmojiViewConfig(
backgroundColor: context.color.surfaceContainer,
+ recentsLimit: 40,
),
searchViewConfig: SearchViewConfig(
backgroundColor: context.color.surfaceContainer,
diff --git a/lib/src/views/components/max_flame_list_title.dart b/lib/src/views/components/max_flame_list_title.dart
index adddb1f..6b4800e 100644
--- a/lib/src/views/components/max_flame_list_title.dart
+++ b/lib/src/views/components/max_flame_list_title.dart
@@ -46,7 +46,7 @@ class _MaxFlameListTitleState extends State {
}
Future _restoreFlames() async {
- if (!isPayingUser(getCurrentPlan())) {
+ if (!isUserAllowed(getCurrentPlan(), PremiumFeatures.RestoreFlames)) {
await context.push(Routes.settingsSubscription);
return;
}
diff --git a/lib/src/views/components/svg_icon.dart b/lib/src/views/components/svg_icon.dart
new file mode 100644
index 0000000..efd067a
--- /dev/null
+++ b/lib/src/views/components/svg_icon.dart
@@ -0,0 +1,31 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+
+class SvgIcons {
+ static const String verifiedGreen = 'assets/icons/verified_badge_green.svg';
+ static const String verifiedYellow = 'assets/icons/verified_badge_yellow.svg';
+ static const String verifiedRed = 'assets/icons/verified_badge_red.svg';
+}
+
+class SvgIcon extends StatelessWidget {
+ const SvgIcon({
+ required this.assetPath,
+ super.key,
+ this.size = 24.0,
+ this.color,
+ });
+ final String assetPath;
+ final double? size;
+ final Color? color;
+
+ @override
+ Widget build(BuildContext context) {
+ return SvgPicture.asset(
+ assetPath,
+ width: size,
+ height: size,
+ colorFilter:
+ color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null,
+ );
+ }
+}
diff --git a/lib/src/views/components/verified_shield.dart b/lib/src/views/components/verified_shield.dart
index 73b6ab2..a18684d 100644
--- a/lib/src/views/components/verified_shield.dart
+++ b/lib/src/views/components/verified_shield.dart
@@ -1,17 +1,17 @@
import 'dart:async';
import 'package:flutter/material.dart';
-import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/twonly.db.dart';
+import 'package:twonly/src/views/components/svg_icon.dart';
class VerifiedShield extends StatefulWidget {
const VerifiedShield({
this.contact,
this.group,
super.key,
- this.size = 18,
+ this.size = 15,
});
final Group? group;
final Contact? contact;
@@ -64,11 +64,13 @@ class _VerifiedShieldState extends State {
message: isVerified
? 'You verified this contact'
: 'You have not verifies this contact.',
- child: FaIcon(
- isVerified ? FontAwesomeIcons.shieldHeart : Icons.gpp_maybe_rounded,
- color:
- isVerified ? Theme.of(context).colorScheme.primary : Colors.red,
- size: widget.size,
+ child: Padding(
+ padding: const EdgeInsetsGeometry.only(top: 2),
+ child: SvgIcon(
+ assetPath:
+ isVerified ? SvgIcons.verifiedGreen : SvgIcons.verifiedRed,
+ size: widget.size,
+ ),
),
),
);
diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart
index 41bc54b..b0fc162 100644
--- a/lib/src/views/contact/contact.view.dart
+++ b/lib/src/views/contact/contact.view.dart
@@ -14,6 +14,7 @@ import 'package:twonly/src/views/components/better_list_title.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/max_flame_list_title.dart';
import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart';
+import 'package:twonly/src/views/components/svg_icon.dart';
import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/groups/group.view.dart';
@@ -163,6 +164,21 @@ class _ContactViewState extends State {
if (getContactDisplayName(contact) != contact.username)
Center(child: Text('(${contact.username})')),
const SizedBox(height: 50),
+ BetterListTile(
+ icon: FontAwesomeIcons.solidComments,
+ text: context.lang.contactViewMessage,
+ onTap: () async {
+ final group =
+ await twonlyDB.groupsDao.getDirectChat(contact.userId);
+ if (group != null && context.mounted) {
+ await context.push(
+ Routes.chatsMessages,
+ extra: group,
+ );
+ }
+ },
+ ),
+ const Divider(),
BetterListTile(
icon: FontAwesomeIcons.pencil,
text: context.lang.contactNickname,
@@ -176,7 +192,6 @@ class _ContactViewState extends State {
}
},
),
- const Divider(),
SelectChatDeletionTimeListTitle(
groupId: getUUIDforDirectChat(widget.userId, gUser.userId),
),
@@ -185,7 +200,11 @@ class _ContactViewState extends State {
contactId: widget.userId,
),
BetterListTile(
- icon: FontAwesomeIcons.shieldHeart,
+ leading: SvgIcon(
+ assetPath: SvgIcons.verifiedGreen,
+ size: 20,
+ color: IconTheme.of(context).color,
+ ),
text: context.lang.contactVerifyNumberTitle,
onTap: () async {
await context.push(Routes.settingsPublicProfile);
diff --git a/lib/src/views/groups/group_create_select_members.view.dart b/lib/src/views/groups/group_create_select_members.view.dart
index 4d1947e..b4d1912 100644
--- a/lib/src/views/groups/group_create_select_members.view.dart
+++ b/lib/src/views/groups/group_create_select_members.view.dart
@@ -1,3 +1,5 @@
+// ignore_for_file: parameter_assignments
+
import 'dart:async';
import 'dart:collection';
import 'package:flutter/material.dart';
diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart
index 19357c9..3a43fd0 100644
--- a/lib/src/views/memories/memories.view.dart
+++ b/lib/src/views/memories/memories.view.dart
@@ -1,3 +1,5 @@
+// ignore_for_file: parameter_assignments
+
import 'dart:async';
import 'package:clock/clock.dart';
import 'package:flutter/material.dart';
diff --git a/lib/src/views/public_profile.view.dart b/lib/src/views/public_profile.view.dart
index e3a937b..a2bb355 100644
--- a/lib/src/views/public_profile.view.dart
+++ b/lib/src/views/public_profile.view.dart
@@ -45,9 +45,29 @@ class _PublicProfileViewState extends State {
body: Column(
children: [
Container(width: double.infinity),
- const SizedBox(
- height: 30,
+ const SizedBox(height: 10),
+ GestureDetector(
+ onTap: () => context.push(Routes.settingsHelpFaqVerifyBadge),
+ child: Container(
+ margin: const EdgeInsets.symmetric(horizontal: 30),
+ padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
+ decoration: BoxDecoration(
+ color: context.color.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Expanded(
+ child: Text(context.lang.verificationBadgeNote),
+ ),
+ const SizedBox(width: 10),
+ const FaIcon(FontAwesomeIcons.angleRight),
+ ],
+ ),
+ ),
),
+ const SizedBox(height: 20),
if (_qrCode != null && _userAvatar != null)
Container(
decoration: BoxDecoration(
diff --git a/lib/src/views/settings/backup/backup_server.view.dart b/lib/src/views/settings/backup/backup_server.view.dart
index 871ed6e..de78081 100644
--- a/lib/src/views/settings/backup/backup_server.view.dart
+++ b/lib/src/views/settings/backup/backup_server.view.dart
@@ -1,4 +1,4 @@
-// ignore_for_file: avoid_dynamic_calls
+// ignore_for_file: parameter_assignments, avoid_dynamic_calls
import 'dart:async';
import 'dart:convert';
diff --git a/lib/src/views/settings/help/diagnostics.view.dart b/lib/src/views/settings/help/diagnostics.view.dart
index 8b05bcf..a96cd1c 100644
--- a/lib/src/views/settings/help/diagnostics.view.dart
+++ b/lib/src/views/settings/help/diagnostics.view.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:path_provider/path_provider.dart';
-import 'package:share_plus/share_plus.dart';
+import 'package:go_router/go_router.dart';
+import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/views/components/loader.dart';
@@ -13,8 +13,6 @@ class DiagnosticsView extends StatefulWidget {
}
class _DiagnosticsViewState extends State {
- final ScrollController _scrollController = ScrollController();
-
String? _debugLogText;
@override
@@ -28,41 +26,6 @@ class _DiagnosticsViewState extends State {
setState(() {});
}
- Future _scrollToBottom() async {
- // Assuming the button is at the bottom of the scroll view
- await _scrollController.animateTo(
- _scrollController.position.maxScrollExtent, // Scroll to the bottom
- duration: const Duration(milliseconds: 300),
- curve: Curves.easeInOut,
- );
- }
-
- Future _shareDebugLog() async {
- if (_debugLogText == null) return;
- final directory = await getApplicationSupportDirectory();
- final logFile = XFile('${directory.path}/app.log');
-
- final params = ShareParams(
- text: 'Debug log',
- files: [logFile],
- );
-
- final result = await SharePlus.instance.share(params);
-
- if (result.status != ShareResultStatus.success) {
- await Clipboard.setData(
- ClipboardData(text: _debugLogText!),
- );
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('Log copied to clipboard!'),
- ),
- );
- }
- }
- }
-
Future _deleteDebugLog() async {
if (await deleteLogFile()) {
if (!mounted) return;
@@ -100,13 +63,10 @@ class _DiagnosticsViewState extends State {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
- onPressed: _shareDebugLog,
+ onPressed: () =>
+ context.push(Routes.settingsHelpContactUs),
child: const Text('Share debug log'),
),
- TextButton(
- onPressed: _scrollToBottom,
- child: const Text('Scroll to Bottom'),
- ),
TextButton(
onPressed: _deleteDebugLog,
child: const Text('Delete Log File'),
@@ -186,7 +146,6 @@ class _LogViewerWidgetState extends State {
selected: selected,
onSelected: (_) => _setFilter(label),
selectedColor: _colorForLevel(label).withAlpha(120),
- backgroundColor: Colors.grey.shade200,
);
}
@@ -198,7 +157,7 @@ class _LogViewerWidgetState extends State {
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
);
- const msgStyle = TextStyle(color: Colors.black87, fontFamily: 'monospace');
+ const msgStyle = TextStyle(fontFamily: 'monospace');
return TextSpan(
children: [
@@ -249,12 +208,7 @@ class _LogViewerWidgetState extends State {
),
Expanded(
child: Container(
- padding: const EdgeInsets.all(8),
- decoration: BoxDecoration(
- color: Colors.grey.shade50,
- borderRadius: BorderRadius.circular(8),
- border: Border.all(color: Colors.grey.shade300),
- ),
+ padding: const EdgeInsets.only(left: 8),
child: Scrollbar(
controller: _controller,
child: ListView.builder(
diff --git a/lib/src/views/settings/help/faq/verifybadge.dart b/lib/src/views/settings/help/faq/verifybadge.dart
new file mode 100644
index 0000000..f3efb3d
--- /dev/null
+++ b/lib/src/views/settings/help/faq/verifybadge.dart
@@ -0,0 +1,62 @@
+import 'package:flutter/material.dart';
+import 'package:twonly/src/utils/misc.dart';
+import 'package:twonly/src/views/components/svg_icon.dart';
+
+class VerificationBadeFaqView extends StatefulWidget {
+ const VerificationBadeFaqView({super.key});
+
+ @override
+ State createState() =>
+ _VerificationBadeFaqViewState();
+}
+
+class _VerificationBadeFaqViewState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(context.lang.verificationBadgeTitle),
+ ),
+ body: ListView(
+ padding: const EdgeInsets.all(40),
+ children: [
+ Text(
+ context.lang.verificationBadgeGeneralDesc,
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 40),
+ _buildItem(
+ icon: const SvgIcon(assetPath: SvgIcons.verifiedGreen, size: 40),
+ description: context.lang.verificationBadgeGreenDesc,
+ ),
+ _buildItem(
+ icon: const SvgIcon(assetPath: SvgIcons.verifiedYellow, size: 40),
+ description: context.lang.verificationBadgeYellowDesc,
+ ),
+ _buildItem(
+ icon: const SvgIcon(assetPath: SvgIcons.verifiedRed, size: 40),
+ description: context.lang.verificationBadgeRedDesc,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildItem({required Widget icon, required String description}) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 40),
+ child: Row(
+ children: [
+ icon,
+ const SizedBox(width: 20),
+ Expanded(
+ child: Text(
+ description,
+ style: const TextStyle(fontSize: 16, height: 1.4),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/src/views/settings/subscription/select_additional_users.view.dart b/lib/src/views/settings/subscription/select_additional_users.view.dart
index 2a6d6f1..f949513 100644
--- a/lib/src/views/settings/subscription/select_additional_users.view.dart
+++ b/lib/src/views/settings/subscription/select_additional_users.view.dart
@@ -1,3 +1,5 @@
+// ignore_for_file: parameter_assignments
+
import 'dart:async';
import 'dart:collection';
import 'package:flutter/material.dart';
diff --git a/lib/src/views/shared/select_contacts.view.dart b/lib/src/views/shared/select_contacts.view.dart
index 6b5a989..c013a6c 100644
--- a/lib/src/views/shared/select_contacts.view.dart
+++ b/lib/src/views/shared/select_contacts.view.dart
@@ -1,3 +1,5 @@
+// ignore_for_file: parameter_assignments
+
import 'dart:async';
import 'dart:collection';
import 'package:flutter/material.dart';
diff --git a/pubspec.yaml b/pubspec.yaml
index 1a48e4e..86dc666 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none'
-version: 0.0.91+91
+version: 0.0.93+93
environment:
sdk: ^3.6.0
@@ -203,6 +203,7 @@ flutter:
assets:
# Add assets from the images directory to the application.
- assets/images/
+ - assets/icons/
- assets/animated_icons/
- assets/animations/
- assets/passwords/
diff --git a/test/features/premium_features.dart b/test/features/premium_features.dart
new file mode 100644
index 0000000..43dda71
--- /dev/null
+++ b/test/features/premium_features.dart
@@ -0,0 +1,17 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:twonly/src/services/subscription.service.dart';
+
+void main() {
+ group('testing subscription permissions', () {
+ test('test if restore flames is allowed', () {
+ expect(
+ true,
+ isUserAllowed(SubscriptionPlan.Plus, PremiumFeatures.RestoreFlames),
+ );
+ expect(
+ false,
+ isUserAllowed(SubscriptionPlan.Free, PremiumFeatures.RestoreFlames),
+ );
+ });
+ });
+}