mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-03-03 10:36:47 +00:00
Merge pull request 'dev' (#4) from dev into main
- 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
This commit is contained in:
commit
6eb636f9dd
40 changed files with 463 additions and 129 deletions
14
CHANGELOG.md
14
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
|
||||
|
|
|
|||
|
|
@ -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/**"
|
||||
|
|
|
|||
4
assets/icons/verified_badge_green.svg
Normal file
4
assets/icons/verified_badge_green.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M10.354 6.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708 0"/>
|
||||
<path d="m10.273 2.513-.921-.944.715-.698.622.637.89-.011a2.89 2.89 0 0 1 2.924 2.924l-.01.89.636.622a2.89 2.89 0 0 1 0 4.134l-.637.622.011.89a2.89 2.89 0 0 1-2.924 2.924l-.89-.01-.622.636a2.89 2.89 0 0 1-4.134 0l-.622-.637-.89.011a2.89 2.89 0 0 1-2.924-2.924l.01-.89-.636-.622a2.89 2.89 0 0 1 0-4.134l.637-.622-.011-.89a2.89 2.89 0 0 1 2.924-2.924l.89.01.622-.636a2.89 2.89 0 0 1 4.134 0l-.715.698a1.89 1.89 0 0 0-2.704 0l-.92.944-1.32-.016a1.89 1.89 0 0 0-1.911 1.912l.016 1.318-.944.921a1.89 1.89 0 0 0 0 2.704l.944.92-.016 1.32a1.89 1.89 0 0 0 1.912 1.911l1.318-.016.921.944a1.89 1.89 0 0 0 2.704 0l.92-.944 1.32.016a1.89 1.89 0 0 0 1.911-1.912l-.016-1.318.944-.921a1.89 1.89 0 0 0 0-2.704l-.944-.92.016-1.32a1.89 1.89 0 0 0-1.912-1.911z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
4
assets/icons/verified_badge_red.svg
Normal file
4
assets/icons/verified_badge_red.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#ff0000" class="bi bi-patch-check" viewBox="0 0 16 16">
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"/>
|
||||
<path d="m10.273 2.513-.921-.944.715-.698.622.637.89-.011a2.89 2.89 0 0 1 2.924 2.924l-.01.89.636.622a2.89 2.89 0 0 1 0 4.134l-.637.622.011.89a2.89 2.89 0 0 1-2.924 2.924l-.89-.01-.622.636a2.89 2.89 0 0 1-4.134 0l-.622-.637-.89.011a2.89 2.89 0 0 1-2.924-2.924l.01-.89-.636-.622a2.89 2.89 0 0 1 0-4.134l.637-.622-.011-.89a2.89 2.89 0 0 1 2.924-2.924l.89.01.622-.636a2.89 2.89 0 0 1 4.134 0l-.715.698a1.89 1.89 0 0 0-2.704 0l-.92.944-1.32-.016a1.89 1.89 0 0 0-1.911 1.912l.016 1.318-.944.921a1.89 1.89 0 0 0 0 2.704l.944.92-.016 1.32a1.89 1.89 0 0 0 1.912 1.911l1.318-.016.921.944a1.89 1.89 0 0 0 2.704 0l.92-.944 1.32.016a1.89 1.89 0 0 0 1.911-1.912l-.016-1.318.944-.921a1.89 1.89 0 0 0 0-2.704l-.944-.92.016-1.32a1.89 1.89 0 0 0-1.912-1.911z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 995 B |
4
assets/icons/verified_badge_yellow.svg
Normal file
4
assets/icons/verified_badge_yellow.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#ffff00" class="bi bi-patch-check" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M10.354 6.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708 0"/>
|
||||
<path d="m10.273 2.513-.921-.944.715-.698.622.637.89-.011a2.89 2.89 0 0 1 2.924 2.924l-.01.89.636.622a2.89 2.89 0 0 1 0 4.134l-.637.622.011.89a2.89 2.89 0 0 1-2.924 2.924l-.89-.01-.622.636a2.89 2.89 0 0 1-4.134 0l-.622-.637-.89.011a2.89 2.89 0 0 1-2.924-2.924l.01-.89-.636-.622a2.89 2.89 0 0 1 0-4.134l.637-.622-.011-.89a2.89 2.89 0 0 1 2.924-2.924l.89.01.622-.636a2.89 2.89 0 0 1 4.134 0l-.715.698a1.89 1.89 0 0 0-2.704 0l-.92.944-1.32-.016a1.89 1.89 0 0 0-1.911 1.912l.016 1.318-.944.921a1.89 1.89 0 0 0 0 2.704l.944.92-.016 1.32a1.89 1.89 0 0 0 1.912 1.911l1.318-.016.921.944a1.89 1.89 0 0 0 2.704 0l.92-.944 1.32.016a1.89 1.89 0 0 0 1.911-1.912l-.016-1.318.944-.921a1.89 1.89 0 0 0 0-2.704l-.944-.92.016-1.32a1.89 1.89 0 0 0-1.912-1.911z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 69d295db737253e0c1b68aedc39bf757e8d642e6
|
||||
Subproject commit 6147155ce50caa97864d56e42e49a6f54702785d
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => '/',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -193,7 +193,11 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<void> _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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,14 @@ Future<bool> 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}.',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -281,6 +281,13 @@ Future<PushNotification?> 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;
|
||||
|
|
|
|||
|
|
@ -10,8 +10,20 @@ enum SubscriptionPlan {
|
|||
Plus,
|
||||
}
|
||||
|
||||
bool isAdditionalAccount(SubscriptionPlan plan) {
|
||||
return plan == SubscriptionPlan.Free || plan == SubscriptionPlan.Plus;
|
||||
enum PremiumFeatures { RestoreFlames }
|
||||
|
||||
const Map<PremiumFeatures, List<SubscriptionPlan>> 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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> _onContactClick() async {
|
||||
Future<void> _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<Color>(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<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
else
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.userPlus,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -509,6 +509,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||
TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)),
|
||||
emojiViewConfig: EmojiViewConfig(
|
||||
backgroundColor: context.color.surfaceContainer,
|
||||
recentsLimit: 40,
|
||||
),
|
||||
searchViewConfig: SearchViewConfig(
|
||||
backgroundColor: context.color.surfaceContainer,
|
||||
|
|
|
|||
|
|
@ -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<MediaViewerView> {
|
|||
}
|
||||
setState(() {});
|
||||
if (firstRun) {
|
||||
// ignore: parameter_assignments
|
||||
firstRun = false;
|
||||
await loadCurrentMediaFile();
|
||||
}
|
||||
|
|
@ -154,7 +155,16 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: parameter_assignments
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
|
|||
}
|
||||
|
||||
Future<void> _restoreFlames() async {
|
||||
if (!isPayingUser(getCurrentPlan())) {
|
||||
if (!isUserAllowed(getCurrentPlan(), PremiumFeatures.RestoreFlames)) {
|
||||
await context.push(Routes.settingsSubscription);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
31
lib/src/views/components/svg_icon.dart
Normal file
31
lib/src/views/components/svg_icon.dart
Normal file
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<VerifiedShield> {
|
|||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<ContactView> {
|
|||
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<ContactView> {
|
|||
}
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
SelectChatDeletionTimeListTitle(
|
||||
groupId: getUUIDforDirectChat(widget.userId, gUser.userId),
|
||||
),
|
||||
|
|
@ -185,7 +200,11 @@ class _ContactViewState extends State<ContactView> {
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: parameter_assignments
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: parameter_assignments
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
|||
|
|
@ -45,9 +45,29 @@ class _PublicProfileViewState extends State<PublicProfileView> {
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// ignore_for_file: avoid_dynamic_calls
|
||||
// ignore_for_file: parameter_assignments, avoid_dynamic_calls
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
|
|
|||
|
|
@ -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<DiagnosticsView> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
String? _debugLogText;
|
||||
|
||||
@override
|
||||
|
|
@ -28,41 +26,6 @@ class _DiagnosticsViewState extends State<DiagnosticsView> {
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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<void> _deleteDebugLog() async {
|
||||
if (await deleteLogFile()) {
|
||||
if (!mounted) return;
|
||||
|
|
@ -100,13 +63,10 @@ class _DiagnosticsViewState extends State<DiagnosticsView> {
|
|||
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<LogViewerWidget> {
|
|||
selected: selected,
|
||||
onSelected: (_) => _setFilter(label),
|
||||
selectedColor: _colorForLevel(label).withAlpha(120),
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -198,7 +157,7 @@ class _LogViewerWidgetState extends State<LogViewerWidget> {
|
|||
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<LogViewerWidget> {
|
|||
),
|
||||
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(
|
||||
|
|
|
|||
62
lib/src/views/settings/help/faq/verifybadge.dart
Normal file
62
lib/src/views/settings/help/faq/verifybadge.dart
Normal file
|
|
@ -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<VerificationBadeFaqView> createState() =>
|
||||
_VerificationBadeFaqViewState();
|
||||
}
|
||||
|
||||
class _VerificationBadeFaqViewState extends State<VerificationBadeFaqView> {
|
||||
@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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: parameter_assignments
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: parameter_assignments
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
17
test/features/premium_features.dart
Normal file
17
test/features/premium_features.dart
Normal file
|
|
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue