mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-03-03 15:06:46 +00:00
Merge pull request 'dev' (#1) from dev into main
- Fixes issue that media files where not reuploaded - Fixes iOS zooming issue when switching between .5 and x1 - Fixes biometric auth bypass when opening a twonly/reopen send image - Fixes that media files could not be downloaded in case the contact deleted his account - Fixes database issue in case twonly is opened multiple times - Fixes typos in translation
This commit is contained in:
commit
8fe00cadbe
34 changed files with 248 additions and 228 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -1,7 +1,8 @@
|
||||||
|
|
||||||
[submodule "dependencies"]
|
[submodule "dependencies"]
|
||||||
path = dependencies
|
path = dependencies
|
||||||
url = https://github.com/twonlyapp/twonly-app-dependencies.git
|
# url = ssh://git@git.twonly.eu:22222/twonly/twonly-app-dependencies.git
|
||||||
|
url = https://git.twonly.eu/twonly/twonly-app-dependencies.git
|
||||||
[submodule "lib/src/localization/translations"]
|
[submodule "lib/src/localization/translations"]
|
||||||
path = lib/src/localization/translations
|
path = lib/src/localization/translations
|
||||||
# url = ssh://git@git.twonly.eu:22222/twonly/twonly-translations.git
|
# url = ssh://git@git.twonly.eu:22222/twonly/twonly-translations.git
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.0.90
|
||||||
|
|
||||||
|
- Fixes issue that media files where not reuploaded
|
||||||
|
- Fixes iOS zooming issue when switching between .5 and x1
|
||||||
|
- Fixes biometric auth bypass when opening a twonly/reopen send image
|
||||||
|
- Fixes that media files could not be downloaded in case the contact deleted his account
|
||||||
|
- Fixes database issue in case twonly is opened multiple times
|
||||||
|
- Fixes typos in translation
|
||||||
|
|
||||||
## 0.0.87
|
## 0.0.87
|
||||||
|
|
||||||
- Adds link preview to images
|
- Adds link preview to images
|
||||||
|
|
|
||||||
27
README.md
27
README.md
|
|
@ -32,13 +32,24 @@ If you decide to give twonly a try, please keep in mind that it is still in its
|
||||||
- Privacy friendly - Everything is stored on the device
|
- Privacy friendly - Everything is stored on the device
|
||||||
- The backend is hosted exclusively in Europe
|
- The backend is hosted exclusively in Europe
|
||||||
|
|
||||||
## Planned
|
## Roadmap
|
||||||
|
|
||||||
- For Android: Optional support for [UnifiedPush](https://unifiedpush.org/)
|
### Currently
|
||||||
- For Android: Reproducible Builds
|
|
||||||
- Implementing [Sealed Sender](https://signal.org/blog/sealed-sender/) to minimize metadata
|
- Focus on user-friendliness so that people enjoy using the app
|
||||||
- Switch from the Signal-Protocol to [MLS](https://github.com/openmls/openmls) for Post-Quantum-Crypto support
|
- User discovery without a phone number
|
||||||
- And, of course, many more features such as dog filters, E2EE cloud backup, and more.
|
- Passwordless recovery without a phone number
|
||||||
|
- Implementation of features so that Snapchat can actually be replaced
|
||||||
|
- E2EE cloud backup of memories
|
||||||
|
- Importing memories from Snapchat
|
||||||
|
|
||||||
|
### Next on the bucket list
|
||||||
|
|
||||||
|
- For Android: Support for [UnifiedPush] (https://unifiedpush.org/)
|
||||||
|
- For Android: Reproducible builds
|
||||||
|
- Implementation of [Sealed Sender](https://signal.org/blog/sealed-sender/) (or a similar protocol) to minimize metadata
|
||||||
|
- Switch from the Signal protocol to [MLS](https://github.com/openmls/openmls) for post-quantum crypto support
|
||||||
|
- Decentralize the server so that anyone can run their own server
|
||||||
|
|
||||||
## Security Issues
|
## Security Issues
|
||||||
|
|
||||||
|
|
@ -46,9 +57,9 @@ If you discover a security issue in twonly, please adhere to the coordinated vul
|
||||||
us your report to security@twonly.eu. We also offer for critical security issues a small bug bounties, but we can not
|
us your report to security@twonly.eu. We also offer for critical security issues a small bug bounties, but we can not
|
||||||
guarantee a bounty currently :/
|
guarantee a bounty currently :/
|
||||||
|
|
||||||
## Contribution
|
<!-- ## Contribution
|
||||||
|
|
||||||
If you have any questions or feature requests, feel free to start a new discussion. Issues are limited to bugs, and for maintainers only.
|
If you have any questions or feature requests, feel free to start a new discussion. Issues are limited to bugs, and for maintainers only. -->
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<data android:scheme="http" />
|
<data android:scheme="http" />
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
<data android:host="*" />
|
<data android:host="*" />
|
||||||
|
|
|
||||||
BIN
assets/filters/beard_upper_lip_green.webp
Normal file
BIN
assets/filters/beard_upper_lip_green.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
BIN
assets/filters/hat_black.webp
Normal file
BIN
assets/filters/hat_black.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4 KiB |
|
|
@ -1 +1 @@
|
||||||
Subproject commit 7930d9727019344238297d810661bc3e8f724c37
|
Subproject commit 3a3a7e5a6323da5413e3dd8c21abfa7cbe1c3a6f
|
||||||
|
|
@ -64,7 +64,19 @@ void main() async {
|
||||||
apiService = ApiService();
|
apiService = ApiService();
|
||||||
twonlyDB = TwonlyDB();
|
twonlyDB = TwonlyDB();
|
||||||
|
|
||||||
|
if (user != null) {
|
||||||
|
if (gUser.appVersion < 90) {
|
||||||
|
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
|
||||||
|
await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState();
|
||||||
|
await updateUserdata((u) {
|
||||||
|
u.appVersion = 90;
|
||||||
|
return u;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await twonlyDB.messagesDao.purgeMessageTable();
|
await twonlyDB.messagesDao.purgeMessageTable();
|
||||||
|
await twonlyDB.receiptsDao.purgeReceivedReceipts();
|
||||||
unawaited(MediaFileService.purgeTempFolder());
|
unawaited(MediaFileService.purgeTempFolder());
|
||||||
|
|
||||||
await initFileDownloader();
|
await initFileDownloader();
|
||||||
|
|
|
||||||
|
|
@ -131,4 +131,18 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
||||||
..limit(100))
|
..limit(100))
|
||||||
.watch();
|
.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateAllRetransmissionUploadingState() async {
|
||||||
|
await (update(mediaFiles)
|
||||||
|
..where(
|
||||||
|
(t) =>
|
||||||
|
t.uploadState.equals(UploadState.uploading.name) &
|
||||||
|
t.reuploadRequestedBy.isNotNull(),
|
||||||
|
))
|
||||||
|
.write(
|
||||||
|
const MediaFilesCompanion(
|
||||||
|
uploadState: Value(UploadState.preprocessing),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,53 +131,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future<List<Message>> getAllMessagesPendingDownloading() {
|
|
||||||
// return (select(messages)
|
|
||||||
// ..where(
|
|
||||||
// (t) =>
|
|
||||||
// t.downloadState.equals(DownloadState.downloaded.index).not() &
|
|
||||||
// t.messageOtherId.isNotNull() &
|
|
||||||
// t.errorWhileSending.equals(false) &
|
|
||||||
// t.kind.equals(MessageKind.media.name),
|
|
||||||
// ))
|
|
||||||
// .get();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Future<List<Message>> getAllNonACKMessagesFromUser() {
|
|
||||||
// return (select(messages)
|
|
||||||
// ..where(
|
|
||||||
// (t) =>
|
|
||||||
// t.acknowledgeByUser.equals(false) &
|
|
||||||
// t.messageOtherId.isNull() &
|
|
||||||
// t.errorWhileSending.equals(false) &
|
|
||||||
// t.sendAt.isBiggerThanValue(
|
|
||||||
// clock.now().subtract(const Duration(minutes: 10)),
|
|
||||||
// ),
|
|
||||||
// ))
|
|
||||||
// .get();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Stream<List<Message>> getAllStoredMediaFiles() {
|
|
||||||
// return (select(messages)
|
|
||||||
// ..where((t) => t.mediaStored.equals(true))
|
|
||||||
// ..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
|
|
||||||
// .watch();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Future<List<Message>> getAllMessagesPendingUpload() {
|
|
||||||
// return (select(messages)
|
|
||||||
// ..where(
|
|
||||||
// (t) =>
|
|
||||||
// t.acknowledgeByServer.equals(false) &
|
|
||||||
// t.messageOtherId.isNull() &
|
|
||||||
// t.mediaUploadId.isNotNull() &
|
|
||||||
// t.downloadState.equals(DownloadState.pending.index) &
|
|
||||||
// t.errorWhileSending.equals(false) &
|
|
||||||
// t.kind.equals(MessageKind.media.name),
|
|
||||||
// ))
|
|
||||||
// .get();
|
|
||||||
// }
|
|
||||||
|
|
||||||
Future<void> openedAllTextMessages(String groupId) {
|
Future<void> openedAllTextMessages(String groupId) {
|
||||||
final updates = MessagesCompanion(openedAt: Value(clock.now()));
|
final updates = MessagesCompanion(openedAt: Value(clock.now()));
|
||||||
return (update(messages)
|
return (update(messages)
|
||||||
|
|
@ -322,32 +275,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
return members.length == actions.length;
|
return members.length == actions.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future<void> updateMessageByOtherUser(
|
|
||||||
// int userId,
|
|
||||||
// int messageId,
|
|
||||||
// MessagesCompanion updatedValues,
|
|
||||||
// ) {
|
|
||||||
// return (update(messages)
|
|
||||||
// ..where(
|
|
||||||
// (c) => c.contactId.equals(userId) & c.messageId.equals(messageId),
|
|
||||||
// ))
|
|
||||||
// .write(updatedValues);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Future<void> updateMessageByOtherMessageId(
|
|
||||||
// int userId,
|
|
||||||
// int messageOtherId,
|
|
||||||
// MessagesCompanion updatedValues,
|
|
||||||
// ) {
|
|
||||||
// return (update(messages)
|
|
||||||
// ..where(
|
|
||||||
// (c) =>
|
|
||||||
// c.contactId.equals(userId) &
|
|
||||||
// c.messageOtherId.equals(messageOtherId),
|
|
||||||
// ))
|
|
||||||
// .write(updatedValues);
|
|
||||||
// }
|
|
||||||
|
|
||||||
Future<void> updateMessageId(
|
Future<void> updateMessageId(
|
||||||
String messageId,
|
String messageId,
|
||||||
MessagesCompanion updatedValues,
|
MessagesCompanion updatedValues,
|
||||||
|
|
@ -445,27 +372,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future<void> deleteMessagesByContactId(int contactId) {
|
|
||||||
// return (delete(messages)
|
|
||||||
// ..where(
|
|
||||||
// (t) => t.contactId.equals(contactId) & t.mediaStored.equals(false),
|
|
||||||
// ))
|
|
||||||
// .go();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Future<void> deleteMessagesByContactIdAndOtherMessageId(
|
|
||||||
// int contactId,
|
|
||||||
// int messageOtherId,
|
|
||||||
// ) {
|
|
||||||
// return (delete(messages)
|
|
||||||
// ..where(
|
|
||||||
// (t) =>
|
|
||||||
// t.contactId.equals(contactId) &
|
|
||||||
// t.messageOtherId.equals(messageOtherId),
|
|
||||||
// ))
|
|
||||||
// .go();
|
|
||||||
// }
|
|
||||||
|
|
||||||
Future<void> deleteMessagesById(String messageId) {
|
Future<void> deleteMessagesById(String messageId) {
|
||||||
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
|
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
|
||||||
}
|
}
|
||||||
|
|
@ -474,24 +380,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
return (delete(messages)..where((t) => t.groupId.equals(groupId))).go();
|
return (delete(messages)..where((t) => t.groupId.equals(groupId))).go();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future<void> deleteAllMessagesByContactId(int contactId) {
|
|
||||||
// return (delete(messages)..where((t) => t.contactId.equals(contactId))).go();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Future<bool> containsOtherMessageId(
|
|
||||||
// int fromUserId,
|
|
||||||
// int messageOtherId,
|
|
||||||
// ) async {
|
|
||||||
// final query = select(messages)
|
|
||||||
// ..where(
|
|
||||||
// (t) =>
|
|
||||||
// t.messageOtherId.equals(messageOtherId) &
|
|
||||||
// t.contactId.equals(fromUserId),
|
|
||||||
// );
|
|
||||||
// final entry = await query.get();
|
|
||||||
// return entry.isNotEmpty;
|
|
||||||
// }
|
|
||||||
|
|
||||||
SingleOrNullSelectable<Message> getMessageById(String messageId) {
|
SingleOrNullSelectable<Message> getMessageById(String messageId) {
|
||||||
return select(messages)..where((t) => t.messageId.equals(messageId));
|
return select(messages)..where((t) => t.messageId.equals(messageId));
|
||||||
}
|
}
|
||||||
|
|
@ -519,31 +407,4 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
|
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
|
||||||
.watch();
|
.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future<List<Message>> getMessagesByMediaUploadId(int mediaUploadId) async {
|
|
||||||
// return (select(messages)
|
|
||||||
// ..where((t) => t.mediaUploadId.equals(mediaUploadId)))
|
|
||||||
// .get();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// SingleOrNullSelectable<Message> getMessageByOtherMessageId(
|
|
||||||
// int fromUserId,
|
|
||||||
// int messageId,
|
|
||||||
// ) {
|
|
||||||
// return select(messages)
|
|
||||||
// ..where(
|
|
||||||
// (t) =>
|
|
||||||
// t.messageOtherId.equals(messageId) & t.contactId.equals(fromUserId),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// SingleOrNullSelectable<Message> getMessageByIdAndContactId(
|
|
||||||
// int fromUserId,
|
|
||||||
// int messageId,
|
|
||||||
// ) {
|
|
||||||
// return select(messages)
|
|
||||||
// ..where(
|
|
||||||
// (t) => t.messageId.equals(messageId) & t.contactId.equals(fromUserId),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,18 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||||
.go();
|
.go();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> purgeReceivedReceipts() async {
|
||||||
|
await (delete(receivedReceipts)
|
||||||
|
..where(
|
||||||
|
(t) => (t.createdAt.isSmallerThanValue(
|
||||||
|
clock.now().subtract(
|
||||||
|
const Duration(days: 25),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.go();
|
||||||
|
}
|
||||||
|
|
||||||
Future<Receipt?> insertReceipt(ReceiptsCompanion entry) async {
|
Future<Receipt?> insertReceipt(ReceiptsCompanion entry) async {
|
||||||
try {
|
try {
|
||||||
var insertEntry = entry;
|
var insertEntry = entry;
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ class TwonlyDB extends _$TwonlyDB {
|
||||||
name: 'twonly',
|
name: 'twonly',
|
||||||
native: const DriftNativeOptions(
|
native: const DriftNativeOptions(
|
||||||
databaseDirectory: getApplicationSupportDirectory,
|
databaseDirectory: getApplicationSupportDirectory,
|
||||||
|
shareAcrossIsolates: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -166,6 +167,7 @@ class TwonlyDB extends _$TwonlyDB {
|
||||||
))
|
))
|
||||||
.go();
|
.go();
|
||||||
await delete(receipts).go();
|
await delete(receipts).go();
|
||||||
|
await delete(receivedReceipts).go();
|
||||||
await update(contacts).write(
|
await update(contacts).write(
|
||||||
const ContactsCompanion(
|
const ContactsCompanion(
|
||||||
avatarSvgCompressed: Value(null),
|
avatarSvgCompressed: Value(null),
|
||||||
|
|
|
||||||
|
|
@ -667,7 +667,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @settingsAccount.
|
/// No description provided for @settingsAccount.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Konto'**
|
/// **'Account'**
|
||||||
String get settingsAccount;
|
String get settingsAccount;
|
||||||
|
|
||||||
/// No description provided for @settingsSubscription.
|
/// No description provided for @settingsSubscription.
|
||||||
|
|
|
||||||
|
|
@ -313,7 +313,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get settingsProfileEditDisplayNameNew => 'New Displayname';
|
String get settingsProfileEditDisplayNameNew => 'New Displayname';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsAccount => 'Konto';
|
String get settingsAccount => 'Account';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsSubscription => 'Subscription';
|
String get settingsSubscription => 'Subscription';
|
||||||
|
|
|
||||||
|
|
@ -313,7 +313,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||||
String get settingsProfileEditDisplayNameNew => 'New Displayname';
|
String get settingsProfileEditDisplayNameNew => 'New Displayname';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsAccount => 'Konto';
|
String get settingsAccount => 'Account';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsSubscription => 'Subscription';
|
String get settingsSubscription => 'Subscription';
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 9d04e9e1d0cdba8f1be4b0cbba341706c3cffac9
|
Subproject commit 4caaa3d91aaf1ac2f13160ba770a2880c26bd229
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||||
|
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||||
import 'package:twonly/src/services/api/utils.dart';
|
import 'package:twonly/src/services/api/utils.dart';
|
||||||
import 'package:twonly/src/services/flame.service.dart';
|
import 'package:twonly/src/services/flame.service.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
|
|
@ -178,9 +179,10 @@ Future<void> handleMediaUpdate(
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
mediaFile.mediaId,
|
mediaFile.mediaId,
|
||||||
MediaFilesCompanion(
|
MediaFilesCompanion(
|
||||||
uploadState: const Value(UploadState.uploading),
|
uploadState: const Value(UploadState.preprocessing),
|
||||||
reuploadRequestedBy: Value(reuploadRequestedBy),
|
reuploadRequestedBy: Value(reuploadRequestedBy),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
unawaited(startBackgroundMediaUpload(MediaFileService(mediaFile)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,44 @@ Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
|
||||||
await twonlyDB.mediaFilesDao.getAllMediaFilesPendingDownload();
|
await twonlyDB.mediaFilesDao.getAllMediaFilesPendingDownload();
|
||||||
|
|
||||||
for (final mediaFile in mediaFiles) {
|
for (final mediaFile in mediaFiles) {
|
||||||
await startDownloadMedia(mediaFile, force);
|
if (await canMediaFileBeDownloaded(mediaFile)) {
|
||||||
|
await startDownloadMedia(mediaFile, force);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> canMediaFileBeDownloaded(MediaFile mediaFile) async {
|
||||||
|
final messages =
|
||||||
|
await twonlyDB.messagesDao.getMessagesByMediaId(mediaFile.mediaId);
|
||||||
|
|
||||||
|
// Verify that the sender of the original image / message does still exists.
|
||||||
|
// If not delete the message as it can not be downloaded from the server anymore.
|
||||||
|
|
||||||
|
if (messages.length != 1) {
|
||||||
|
Log.error('A media for download must have one original message.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.first.senderId == null) {
|
||||||
|
Log.error('A media for download must have a sender id.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final contact =
|
||||||
|
await twonlyDB.contactsDao.getContactById(messages.first.senderId!);
|
||||||
|
|
||||||
|
if (contact == null || contact.accountDeleted) {
|
||||||
|
Log.info(
|
||||||
|
'Sender does not exists anymore. Delete media file and message.',
|
||||||
|
);
|
||||||
|
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
|
||||||
|
await twonlyDB.messagesDao.deleteMessagesById(messages.first.messageId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
enum DownloadMediaTypes {
|
enum DownloadMediaTypes {
|
||||||
video,
|
video,
|
||||||
image,
|
image,
|
||||||
|
|
@ -90,11 +124,9 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
|
||||||
failed = false;
|
failed = false;
|
||||||
} else {
|
} else {
|
||||||
failed = true;
|
failed = true;
|
||||||
if (update.responseStatusCode != null) {
|
Log.error(
|
||||||
Log.error(
|
'Got invalid response status code: ${update.responseStatusCode}',
|
||||||
'Got invalid response status code: ${update.responseStatusCode}',
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.info('Got ${update.status} for $mediaId');
|
Log.info('Got ${update.status} for $mediaId');
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,14 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
}
|
}
|
||||||
receiptId = receipt.receiptId;
|
receiptId = receipt.receiptId;
|
||||||
|
|
||||||
|
final contact =
|
||||||
|
await twonlyDB.contactsDao.getContactById(receipt.contactId);
|
||||||
|
if (contact == null || contact.accountDeleted) {
|
||||||
|
Log.warn('Will not send message again as user does not exist anymore.');
|
||||||
|
await twonlyDB.receiptsDao.deleteReceipt(receiptId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!onlyReturnEncryptedData &&
|
if (!onlyReturnEncryptedData &&
|
||||||
receipt.ackByServerAt != null &&
|
receipt.ackByServerAt != null &&
|
||||||
receipt.markForRetry == null) {
|
receipt.markForRetry == null) {
|
||||||
|
|
@ -177,9 +185,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
if (receiptId != null) {
|
if (receiptId != null) {
|
||||||
await twonlyDB.receiptsDao.deleteReceipt(receiptId);
|
await twonlyDB.receiptsDao.deleteReceipt(receiptId);
|
||||||
}
|
}
|
||||||
if (receipt != null) {
|
|
||||||
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,17 @@ import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||||
import 'package:twonly/src/views/contact/contact.view.dart';
|
import 'package:twonly/src/views/contact/contact.view.dart';
|
||||||
import 'package:twonly/src/views/public_profile.view.dart';
|
import 'package:twonly/src/views/public_profile.view.dart';
|
||||||
|
|
||||||
Future<void> handleIntentUrl(BuildContext context, Uri uri) async {
|
Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
||||||
if (!uri.scheme.startsWith('http')) return;
|
if (!uri.scheme.startsWith('http')) return false;
|
||||||
if (uri.host != 'me.twonly.eu') return;
|
if (uri.host != 'me.twonly.eu') return false;
|
||||||
if (uri.hasEmptyPath) return;
|
if (uri.hasEmptyPath) return false;
|
||||||
|
|
||||||
final publicKey = uri.hasFragment ? uri.fragment : null;
|
final publicKey = uri.hasFragment ? uri.fragment : null;
|
||||||
final userPaths = uri.path.split('/');
|
final userPaths = uri.path.split('/');
|
||||||
if (userPaths.length != 2) return;
|
if (userPaths.length != 2) return false;
|
||||||
final username = userPaths[1];
|
final username = userPaths[1];
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return false;
|
||||||
|
|
||||||
if (username == gUser.username) {
|
if (username == gUser.username) {
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
|
|
@ -40,7 +40,7 @@ Future<void> handleIntentUrl(BuildContext context, Uri uri) async {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.info(
|
Log.info(
|
||||||
|
|
@ -48,7 +48,7 @@ Future<void> handleIntentUrl(BuildContext context, Uri uri) async {
|
||||||
);
|
);
|
||||||
final contacts = await twonlyDB.contactsDao.getContactsByUsername(username);
|
final contacts = await twonlyDB.contactsDao.getContactsByUsername(username);
|
||||||
if (contacts.isEmpty) {
|
if (contacts.isEmpty) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return true;
|
||||||
Uint8List? publicKeyBytes;
|
Uint8List? publicKeyBytes;
|
||||||
if (publicKey != null) {
|
if (publicKey != null) {
|
||||||
publicKeyBytes = base64Url.decode(publicKey);
|
publicKeyBytes = base64Url.decode(publicKey);
|
||||||
|
|
@ -72,7 +72,7 @@ Future<void> handleIntentUrl(BuildContext context, Uri uri) async {
|
||||||
if (storedPublicKey == null ||
|
if (storedPublicKey == null ||
|
||||||
receivedPublicKey.isEmpty ||
|
receivedPublicKey.isEmpty ||
|
||||||
!context.mounted) {
|
!context.mounted) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
if (storedPublicKey.equals(receivedPublicKey)) {
|
if (storedPublicKey.equals(receivedPublicKey)) {
|
||||||
if (!contact.verified) {
|
if (!contact.verified) {
|
||||||
|
|
@ -112,6 +112,7 @@ Future<void> handleIntentUrl(BuildContext context, Uri uri) async {
|
||||||
Log.warn(e);
|
Log.warn(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleIntentMediaFile(
|
Future<void> handleIntentMediaFile(
|
||||||
|
|
@ -160,12 +161,15 @@ Future<void> handleIntentSharedFile(
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Log.info('got file via intent ${file.type} ${file.value}');
|
|
||||||
|
Log.info('got file via intent ${file.type}');
|
||||||
|
|
||||||
switch (file.type) {
|
switch (file.type) {
|
||||||
case SharedMediaType.URL:
|
case SharedMediaType.URL:
|
||||||
if (file.value?.startsWith('http') ?? false) {
|
if (file.value?.startsWith('http') ?? false) {
|
||||||
onUrlCallBack(Uri.parse(file.value!));
|
final uri = Uri.parse(file.value!);
|
||||||
|
Log.info('Got link via handle intent share file: ${uri.scheme}');
|
||||||
|
onUrlCallBack(uri);
|
||||||
}
|
}
|
||||||
case SharedMediaType.IMAGE:
|
case SharedMediaType.IMAGE:
|
||||||
var type = MediaType.image;
|
var type = MediaType.image;
|
||||||
|
|
|
||||||
|
|
@ -159,8 +159,12 @@ Future<bool> authenticateUser(
|
||||||
}
|
}
|
||||||
} on LocalAuthException catch (e) {
|
} on LocalAuthException catch (e) {
|
||||||
Log.error(e.toString());
|
Log.error(e.toString());
|
||||||
if (!force) {
|
if (e.code == LocalAuthExceptionCode.noBiometricHardware ||
|
||||||
return true;
|
e.code == LocalAuthExceptionCode.noBiometricsEnrolled ||
|
||||||
|
e.code == LocalAuthExceptionCode.noCredentialsSet) {
|
||||||
|
if (!force) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:twonly/src/views/camera/camera_preview_components/painters/face_
|
||||||
enum FaceFilterType {
|
enum FaceFilterType {
|
||||||
none,
|
none,
|
||||||
dogBrown,
|
dogBrown,
|
||||||
|
beardUpperLipGreen,
|
||||||
beardUpperLip,
|
beardUpperLip,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,7 +28,9 @@ extension FaceFilterTypeExtension on FaceFilterType {
|
||||||
case FaceFilterType.dogBrown:
|
case FaceFilterType.dogBrown:
|
||||||
return DogFilterPainter.getPreview();
|
return DogFilterPainter.getPreview();
|
||||||
case FaceFilterType.beardUpperLip:
|
case FaceFilterType.beardUpperLip:
|
||||||
return BeardFilterPainter.getPreview();
|
return BeardFilterPainter.getPreview(this);
|
||||||
|
case FaceFilterType.beardUpperLipGreen:
|
||||||
|
return BeardFilterPainter.getPreview(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,11 @@ class MainCameraController {
|
||||||
setState();
|
setState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onImageSend() {
|
||||||
|
scannedUrl = '';
|
||||||
|
setState();
|
||||||
|
}
|
||||||
|
|
||||||
final BarcodeScanner _barcodeScanner = BarcodeScanner();
|
final BarcodeScanner _barcodeScanner = BarcodeScanner();
|
||||||
final FaceDetector _faceDetector = FaceDetector(
|
final FaceDetector _faceDetector = FaceDetector(
|
||||||
options: FaceDetectorOptions(
|
options: FaceDetectorOptions(
|
||||||
|
|
@ -78,7 +83,7 @@ class MainCameraController {
|
||||||
CustomPaint? facePaint;
|
CustomPaint? facePaint;
|
||||||
Offset? focusPointOffset;
|
Offset? focusPointOffset;
|
||||||
|
|
||||||
FaceFilterType _currentFilterType = FaceFilterType.beardUpperLip;
|
FaceFilterType _currentFilterType = FaceFilterType.none;
|
||||||
FaceFilterType get currentFilterType => _currentFilterType;
|
FaceFilterType get currentFilterType => _currentFilterType;
|
||||||
|
|
||||||
Future<void> closeCamera() async {
|
Future<void> closeCamera() async {
|
||||||
|
|
@ -392,20 +397,25 @@ class MainCameraController {
|
||||||
cameraController != null) {
|
cameraController != null) {
|
||||||
if (faces.isNotEmpty) {
|
if (faces.isNotEmpty) {
|
||||||
CustomPainter? painter;
|
CustomPainter? painter;
|
||||||
if (_currentFilterType == FaceFilterType.dogBrown) {
|
switch (_currentFilterType) {
|
||||||
painter = DogFilterPainter(
|
case FaceFilterType.dogBrown:
|
||||||
faces,
|
painter = DogFilterPainter(
|
||||||
inputImage.metadata!.size,
|
faces,
|
||||||
inputImage.metadata!.rotation,
|
inputImage.metadata!.size,
|
||||||
cameraController!.description.lensDirection,
|
inputImage.metadata!.rotation,
|
||||||
);
|
cameraController!.description.lensDirection,
|
||||||
} else if (_currentFilterType == FaceFilterType.beardUpperLip) {
|
);
|
||||||
painter = BeardFilterPainter(
|
case FaceFilterType.beardUpperLip:
|
||||||
faces,
|
case FaceFilterType.beardUpperLipGreen:
|
||||||
inputImage.metadata!.size,
|
painter = BeardFilterPainter(
|
||||||
inputImage.metadata!.rotation,
|
_currentFilterType,
|
||||||
cameraController!.description.lensDirection,
|
faces,
|
||||||
);
|
inputImage.metadata!.size,
|
||||||
|
inputImage.metadata!.rotation,
|
||||||
|
cameraController!.description.lensDirection,
|
||||||
|
);
|
||||||
|
case FaceFilterType.none:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (painter != null) {
|
if (painter != null) {
|
||||||
|
|
|
||||||
|
|
@ -6,27 +6,45 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
import 'package:twonly/src/views/camera/camera_preview_components/face_filters.dart';
|
||||||
import 'package:twonly/src/views/camera/camera_preview_components/painters/coordinates_translator.dart';
|
import 'package:twonly/src/views/camera/camera_preview_components/painters/coordinates_translator.dart';
|
||||||
import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart';
|
import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart';
|
||||||
|
|
||||||
class BeardFilterPainter extends FaceFilterPainter {
|
class BeardFilterPainter extends FaceFilterPainter {
|
||||||
BeardFilterPainter(
|
BeardFilterPainter(
|
||||||
|
FaceFilterType beardType,
|
||||||
super.faces,
|
super.faces,
|
||||||
super.imageSize,
|
super.imageSize,
|
||||||
super.rotation,
|
super.rotation,
|
||||||
super.cameraLensDirection,
|
super.cameraLensDirection,
|
||||||
) {
|
) {
|
||||||
_loadAssets();
|
_loadAssets(beardType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static FaceFilterType? _lastLoadedBeardType;
|
||||||
static ui.Image? _beardImage;
|
static ui.Image? _beardImage;
|
||||||
static bool _loading = false;
|
static bool _loading = false;
|
||||||
|
|
||||||
static Future<void> _loadAssets() async {
|
static String getAssetPath(FaceFilterType beardType) {
|
||||||
if (_loading || _beardImage != null) return;
|
switch (beardType) {
|
||||||
|
case FaceFilterType.beardUpperLip:
|
||||||
|
return 'assets/filters/beard_upper_lip.webp';
|
||||||
|
case FaceFilterType.beardUpperLipGreen:
|
||||||
|
return 'assets/filters/beard_upper_lip_green.webp';
|
||||||
|
case FaceFilterType.dogBrown:
|
||||||
|
case FaceFilterType.none:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _loadAssets(FaceFilterType beardType) async {
|
||||||
|
if ((_loading || _beardImage != null) &&
|
||||||
|
_lastLoadedBeardType == beardType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_loading = true;
|
_loading = true;
|
||||||
try {
|
try {
|
||||||
_beardImage = await _loadImage('assets/filters/beard_upper_lip.webp');
|
_beardImage = await _loadImage(getAssetPath(beardType));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Failed to load filter assets: $e');
|
Log.error('Failed to load filter assets: $e');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -161,12 +179,12 @@ class BeardFilterPainter extends FaceFilterPainter {
|
||||||
..restore();
|
..restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Widget getPreview() {
|
static Widget getPreview(FaceFilterType beardType) {
|
||||||
return Preview(
|
return Preview(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/filters/beard_upper_lip.webp',
|
getAssetPath(beardType),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,8 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (showWideAngleZoomIOS &&
|
if (showWideAngleZoomIOS &&
|
||||||
widget.selectedCameraDetails.cameraId == 2) {
|
widget.selectedCameraDetails.cameraId ==
|
||||||
|
_wideCameraIndex) {
|
||||||
await widget.selectCamera(0, true);
|
await widget.selectCamera(0, true);
|
||||||
} else {
|
} else {
|
||||||
widget.updateScaleFactor(1.0);
|
widget.updateScaleFactor(1.0);
|
||||||
|
|
@ -175,6 +176,12 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
||||||
final level =
|
final level =
|
||||||
min(await widget.controller.getMaxZoomLevel(), 2)
|
min(await widget.controller.getMaxZoomLevel(), 2)
|
||||||
.toDouble();
|
.toDouble();
|
||||||
|
|
||||||
|
if (showWideAngleZoomIOS &&
|
||||||
|
widget.selectedCameraDetails.cameraId ==
|
||||||
|
_wideCameraIndex) {
|
||||||
|
await widget.selectCamera(0, true);
|
||||||
|
}
|
||||||
widget.updateScaleFactor(level);
|
widget.updateScaleFactor(level);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
|
||||||
|
|
@ -424,6 +424,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
),
|
),
|
||||||
) as bool?;
|
) as bool?;
|
||||||
if (wasSend != null && wasSend && mounted) {
|
if (wasSend != null && wasSend && mounted) {
|
||||||
|
widget.mainCameraController?.onImageSend();
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
} else {
|
} else {
|
||||||
await videoController?.play();
|
await videoController?.play();
|
||||||
|
|
@ -591,6 +592,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
widget.mainCameraController?.onImageSend();
|
||||||
|
|
||||||
// must be awaited so the widget for the screenshot is not already disposed when sending..
|
// must be awaited so the widget for the screenshot is not already disposed when sending..
|
||||||
await storeImageAsOriginal();
|
await storeImageAsOriginal();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class _BackgroundLayerState extends State<BackgroundLayer> {
|
||||||
width: widget.layerData.image.width.toDouble(),
|
width: widget.layerData.image.width.toDouble(),
|
||||||
height: widget.layerData.image.height.toDouble(),
|
height: widget.layerData.image.height.toDouble(),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
color: Colors.green,
|
color: Colors.transparent,
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: UiImagePainter(scImage.image!),
|
painter: UiImagePainter(scImage.image!),
|
||||||
),
|
),
|
||||||
|
|
@ -47,16 +47,25 @@ class UiImagePainter extends CustomPainter {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final imageSize = Size(image.width.toDouble(), image.height.toDouble());
|
||||||
|
|
||||||
|
final sizes = applyBoxFit(BoxFit.contain, imageSize, size);
|
||||||
|
|
||||||
|
final destRect = Alignment.center.inscribe(
|
||||||
|
sizes.destination,
|
||||||
|
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||||
|
);
|
||||||
|
|
||||||
canvas.drawImageRect(
|
canvas.drawImageRect(
|
||||||
image,
|
image,
|
||||||
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
|
Rect.fromLTWH(0, 0, imageSize.width, imageSize.height),
|
||||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
destRect,
|
||||||
Paint(),
|
Paint(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
bool shouldRepaint(covariant UiImagePainter oldDelegate) {
|
||||||
return false;
|
return image != oldDelegate.image;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,10 @@ class MastodonPostCard extends StatelessWidget {
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
if (info.desc != null && info.desc != 'null')
|
if (info.desc != null && info.desc != 'null')
|
||||||
Text(
|
Text(
|
||||||
substringBy(info.desc!, 1000),
|
substringBy(
|
||||||
|
info.desc!.replaceAll('Attached: 1 image', '').trim(),
|
||||||
|
info.image == null ? 500 : 300,
|
||||||
|
),
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||||
),
|
),
|
||||||
if (info.image != null && info.image != 'null')
|
if (info.image != null && info.image != 'null')
|
||||||
|
|
@ -57,7 +60,7 @@ class MastodonPostCard extends StatelessWidget {
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxHeight: 250),
|
constraints: const BoxConstraints(maxHeight: 200),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: info.image!,
|
imageUrl: info.image!,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ class TwitterPostCard extends StatelessWidget {
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (info.desc != null && info.desc != 'null')
|
if (info.desc != null && info.desc != 'null')
|
||||||
Text(
|
Text(
|
||||||
substringBy(info.desc!, 1000),
|
substringBy(info.desc!, info.image == null ? 500 : 300),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: primaryText,
|
color: primaryText,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
|
|
@ -73,7 +73,7 @@ class TwitterPostCard extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
),
|
),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxHeight: 300),
|
constraints: const BoxConstraints(maxHeight: 200),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: info.image!,
|
imageUrl: info.image!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ class HomeViewState extends State<HomeView> {
|
||||||
if (offsetRatio == 1) {
|
if (offsetRatio == 1) {
|
||||||
disableCameraTimer = Timer(const Duration(milliseconds: 500), () async {
|
disableCameraTimer = Timer(const Duration(milliseconds: 500), () async {
|
||||||
await _mainCameraController.closeCamera();
|
await _mainCameraController.closeCamera();
|
||||||
|
_mainCameraController.sharedLinkForPreview = null;
|
||||||
disableCameraTimer = null;
|
disableCameraTimer = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +116,14 @@ class HomeViewState extends State<HomeView> {
|
||||||
|
|
||||||
// Subscribe to all events (initial link and further)
|
// Subscribe to all events (initial link and further)
|
||||||
_deepLinkSub = AppLinks().uriLinkStream.listen((uri) async {
|
_deepLinkSub = AppLinks().uriLinkStream.listen((uri) async {
|
||||||
if (mounted) await handleIntentUrl(context, uri);
|
if (mounted) {
|
||||||
|
Log.info('Got link via app links: ${uri.scheme}');
|
||||||
|
if (!await handleIntentUrl(context, uri)) {
|
||||||
|
if (uri.scheme.startsWith('http')) {
|
||||||
|
_mainCameraController.setSharedLinkForPreview(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_intentStreamSub = FlutterSharingIntent.instance.getMediaStream().listen(
|
_intentStreamSub = FlutterSharingIntent.instance.getMediaStream().listen(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:http/http.dart' as http;
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/utils/keyvalue.dart';
|
import 'package:twonly/src/utils/keyvalue.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
|
||||||
const userStudySurveyKey = 'user_study_survey';
|
const userStudySurveyKey = 'user_study_survey';
|
||||||
|
|
@ -34,9 +35,8 @@ Future<void> handleUserStudyUpload() async {
|
||||||
await KeyValueStore.delete(userStudySurveyKey);
|
await KeyValueStore.delete(userStudySurveyKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gUser.lastUserStudyDataUpload
|
if (gUser.lastUserStudyDataUpload != null &&
|
||||||
?.isAfter(DateTime.now().subtract(const Duration(days: 1))) ??
|
isToday(gUser.lastUserStudyDataUpload!)) {
|
||||||
false) {
|
|
||||||
// Only send updates once a day.
|
// Only send updates once a day.
|
||||||
// This enables to see if improvements to actually work.
|
// This enables to see if improvements to actually work.
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@ class UserStudyQuestionnaire extends StatefulWidget {
|
||||||
|
|
||||||
class _UserStudyQuestionnaireState extends State<UserStudyQuestionnaire> {
|
class _UserStudyQuestionnaireState extends State<UserStudyQuestionnaire> {
|
||||||
final Map<String, dynamic> _responses = {
|
final Map<String, dynamic> _responses = {
|
||||||
'gender': null,
|
|
||||||
'gender_free': '',
|
|
||||||
'age': null,
|
'age': null,
|
||||||
'education': null,
|
'education': null,
|
||||||
'education_free': '',
|
'education_free': '',
|
||||||
|
|
@ -51,7 +49,9 @@ class _UserStudyQuestionnaireState extends State<UserStudyQuestionnaire> {
|
||||||
|
|
||||||
await updateUserdata((u) {
|
await updateUserdata((u) {
|
||||||
// generate a random participants id to identify data send later while keeping the user anonym
|
// generate a random participants id to identify data send later while keeping the user anonym
|
||||||
u.userStudyParticipantsToken = getRandomString(25);
|
u
|
||||||
|
..userStudyParticipantsToken = getRandomString(25)
|
||||||
|
..askedForUserStudyPermission = true;
|
||||||
return u;
|
return u;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -75,15 +75,6 @@ class _UserStudyQuestionnaireState extends State<UserStudyQuestionnaire> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_sectionTitle('Demografische Daten'),
|
_sectionTitle('Demografische Daten'),
|
||||||
_questionText('Was ist dein Geschlecht?'),
|
|
||||||
_buildRadioList(
|
|
||||||
['Männlich', 'Weiblich', 'Divers', 'Keine Angabe'],
|
|
||||||
'gender',
|
|
||||||
),
|
|
||||||
_buildTextField(
|
|
||||||
'Freitext (optional)',
|
|
||||||
(val) => _responses['gender_free'] = val,
|
|
||||||
),
|
|
||||||
_questionText('Wie alt bist du?'),
|
_questionText('Wie alt bist du?'),
|
||||||
_buildRadioList(
|
_buildRadioList(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -905,7 +905,7 @@ packages:
|
||||||
path: "dependencies/hashlib"
|
path: "dependencies/hashlib"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "2.2.0"
|
version: "2.3.0"
|
||||||
hashlib_codecs:
|
hashlib_codecs:
|
||||||
dependency: "direct overridden"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
|
|
@ -1259,7 +1259,7 @@ packages:
|
||||||
path: "dependencies/no_screenshot"
|
path: "dependencies/no_screenshot"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.3.2-beta.3"
|
version: "0.3.2"
|
||||||
objective_c:
|
objective_c:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 0.0.87+87
|
version: 0.0.90+90
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.6.0
|
sdk: ^3.6.0
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue