diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3c08604..286f91d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -152,12 +152,10 @@ upload_to_fdroid_repo: - chmod 700 ~/.ssh - ssh-keyscan -t rsa fdroid.nordgedanken.dev >> ~/.ssh/known_hosts script: - - mkdir -p upload - - cp build/android/* upload/ - cd build/android/ - - export UPDATE_VERSION=$(pcregrep -o1 'version:\\s([0-9]*\\.[0-9]*\\.[0-9]*)\\+[0-9]*' pubspec.yaml) && mv app-release.apk "${UPDATE_VERSION}.apk" + - export UPDATE_VERSION=$(pcregrep -o1 'version:\\s([0-9]*\\.[0-9]*\\.[0-9]*)\\+[0-9]*' ../../pubspec.yaml) && mv app-release.apk "${UPDATE_VERSION}.apk" - rsync -rav -e ssh ./ fluffy@fdroid.nordgedanken.dev:/fdroid/repo - - ssh fluffy@fdroid.nordgedanken.dev "cd fdroid && mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc && fdroid update" + - ssh fluffy@fdroid.nordgedanken.dev "cd fdroid && fdroid update" needs: ["build_android_apk"] only: - tags @@ -196,6 +194,29 @@ build_linux: - build/linux/release/bundle/ only: - main + +snap:edge: + stage: publish + image: "cibuilds/snapcraft:core18" + only: + - main + script: + ## Manually install the flutter-dev snap, so we can use the flutter extension + - 'curl -L $(curl -H "X-Ubuntu-Series: 16" "https://api.snapcraft.io/api/v1/snaps/details/flutter?channel=latest/stable" | jq ".download_url" -r) --output flutter.snap' + - sudo mkdir -p /snap/flutter + - sudo unsquashfs -d /snap/flutter/current flutter.snap + - rm -f flutter.snap + - sudo ln -sf /snap/flutter/current/flutter.sh /snap/bin/flutter + - sudo ln -sf /snap/flutter/current/env.sh /snap/bin/env.sh + - snapcraft + - echo $SNAPCRAFT_LOGIN_FILE | base64 --decode --ignore-garbage > snapcraft.login + - snapcraft login --with snapcraft.login + - snapcraft push --release=edge *.snap + - snapcraft logout + artifacts: + paths: + - './*.snap' + when: on_success snap:publish: stage: publish diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d11148..f7fac2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,16 @@ -# Version 0.20.0 - 2020-??-?? +# Version 0.21.0 - 2020-10-28 +### Features +- New user viewer +- Add code syntax highlighting in messages +- Updated translations: Thanks to all helpers +### Changes +- Stories feature removed +### Fixes +- Fixes sentry +- Fixes Android download +- Minor fixes + +# Version 0.20.0 - 2020-10-23 ### Features - Added translations: Arabic - Add ability to enable / disable emotes globally @@ -18,6 +30,7 @@ - Show device name in account information correctly - Fix tapping on aliases / room pills not always working - Link clicking in web not always working +- Return message input field to previous state after editing message - Thanks @inexcode # Version 0.19.0 - 2020-09-21 ### Features diff --git a/README.md b/README.md index 5c3a6c5..843eb45 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,6 @@ cd FurryChat sudo apt install ninja-build ``` -* Outcomment the Google Services plugin at the end of the file `android/app/build.gradle`: -``` -// apply plugin: "com.google.gms.google-services" -``` - * Build with: `flutter build apk` ### iOS / iPadOS diff --git a/android/app/build.gradle b/android/app/build.gradle index 3d72600..b6b03c4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 28 + compileSdkVersion 30 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -44,8 +44,8 @@ android { defaultConfig { applicationId "dev.inex.furrychat" - minSdkVersion 18 - targetSdkVersion 28 + minSdkVersion 21 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -87,4 +87,4 @@ dependencies { implementation "net.zetetic:android-database-sqlcipher:4.4.0" // needed for moor_ffi w/ sqlcipher } -apply plugin: "com.google.gms.google-services" +apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/assets/logo.svg b/assets/logo.svg index 93d5231..a3a9516 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,48 +1,43 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/lib/components/avatar.dart b/lib/components/avatar.dart index 9e51f4f..cef8855 100644 --- a/lib/components/avatar.dart +++ b/lib/components/avatar.dart @@ -45,10 +45,12 @@ class Avatar extends StatelessWidget { ), ); final noPic = mxContent == null || mxContent.toString().isEmpty; + final borderRadius = BorderRadius.circular(size / 2); return InkWell( onTap: onTap, + borderRadius: borderRadius, child: ClipRRect( - borderRadius: BorderRadius.circular(size / 2), + borderRadius: borderRadius, child: Container( width: size, height: size, @@ -68,6 +70,11 @@ class Avatar extends StatelessWidget { textWidget, ], ), + errorWidget: (c, s, d) => Stack( + children: [ + textWidget, + ], + ), ), ), ), diff --git a/lib/components/dialogs/send_file_dialog.dart b/lib/components/dialogs/send_file_dialog.dart index 90cd3fc..28d1505 100644 --- a/lib/components/dialogs/send_file_dialog.dart +++ b/lib/components/dialogs/send_file_dialog.dart @@ -19,7 +19,7 @@ class SendFileDialog extends StatefulWidget { class _SendFileDialogState extends State { bool origImage = false; - + bool _isSending = false; Future _send() async { var file = widget.file; if (file is MatrixImageFile && !origImage) { @@ -82,10 +82,16 @@ class _SendFileDialogState extends State { ), FlatButton( child: Text(L10n.of(context).send), - onPressed: () async { - await SimpleDialogs(context).tryRequestWithLoadingDialog(_send()); - await Navigator.of(context).pop(); - }, + onPressed: _isSending + ? null + : () async { + setState(() { + _isSending = true; + }); + await SimpleDialogs(context) + .tryRequestWithLoadingDialog(_send()); + await Navigator.of(context).pop(); + }, ), ], ); diff --git a/lib/components/html_message.dart b/lib/components/html_message.dart index df508a4..a67377a 100644 --- a/lib/components/html_message.dart +++ b/lib/components/html_message.dart @@ -33,6 +33,8 @@ class HtmlMessage extends StatelessWidget { // there is no need to pre-validate the html, as we validate it while rendering + final matrix = Matrix.of(context); + final themeData = Theme.of(context); return Html( data: renderHtml, @@ -50,12 +52,18 @@ class HtmlMessage extends StatelessWidget { getMxcUrl: (String mxc, double width, double height) { final ratio = MediaQuery.of(context).devicePixelRatio; return Uri.parse(mxc)?.getThumbnail( - Matrix.of(context).client, + matrix.client, width: (width ?? 800) * ratio, height: (height ?? 800) * ratio, method: ThumbnailMethod.scale, ); }, + setCodeLanguage: (String key, String value) async { + await matrix.store.setItem('code_language.$key', value); + }, + getCodeLanguage: (String key) async { + return await matrix.store.getItem('code_language.$key'); + }, getPillInfo: (String identifier) async { if (room == null) { return null; diff --git a/lib/components/list_items/participant_list_item.dart b/lib/components/list_items/participant_list_item.dart index e890f8f..907ed5b 100644 --- a/lib/components/list_items/participant_list_item.dart +++ b/lib/components/list_items/participant_list_item.dart @@ -7,60 +7,13 @@ import '../../views/chat.dart'; import '../avatar.dart'; import '../dialogs/simple_dialogs.dart'; import '../matrix.dart'; +import '../user_bottom_sheet.dart'; class ParticipantListItem extends StatelessWidget { final User user; const ParticipantListItem(this.user); - void participantAction(BuildContext context, String action) async { - switch (action) { - case 'ban': - if (await SimpleDialogs(context).askConfirmation()) { - await SimpleDialogs(context).tryRequestWithLoadingDialog(user.ban()); - } - break; - case 'unban': - if (await SimpleDialogs(context).askConfirmation()) { - await SimpleDialogs(context) - .tryRequestWithLoadingDialog(user.unban()); - } - break; - case 'kick': - if (await SimpleDialogs(context).askConfirmation()) { - await SimpleDialogs(context).tryRequestWithLoadingDialog(user.kick()); - } - break; - case 'admin': - if (await SimpleDialogs(context).askConfirmation()) { - await SimpleDialogs(context) - .tryRequestWithLoadingDialog(user.setPower(100)); - } - break; - case 'moderator': - if (await SimpleDialogs(context).askConfirmation()) { - await SimpleDialogs(context) - .tryRequestWithLoadingDialog(user.setPower(50)); - } - break; - case 'user': - if (await SimpleDialogs(context).askConfirmation()) { - await SimpleDialogs(context) - .tryRequestWithLoadingDialog(user.setPower(0)); - } - break; - case 'message': - final roomId = await user.startDirectChat(); - await Navigator.of(context).pushAndRemoveUntil( - AppRoute.defaultRoute( - context, - ChatView(roomId), - ), - (Route r) => r.isFirst); - break; - } - } - @override Widget build(BuildContext context) { var membershipBatch = { @@ -74,87 +27,43 @@ class ParticipantListItem extends StatelessWidget { : user.powerLevel >= 50 ? L10n.of(context).moderator : ''; - var items = >[]; - if (user.id != Matrix.of(context).client.userID) { - items.add( - PopupMenuItem( - child: Text(L10n.of(context).sendAMessage), value: 'message'), - ); - } - if (user.canChangePowerLevel && - user.room.ownPowerLevel == 100 && - user.powerLevel != 100) { - items.add( - PopupMenuItem( - child: Text(L10n.of(context).makeAnAdmin), value: 'admin'), - ); - } - if (user.canChangePowerLevel && - user.room.ownPowerLevel >= 50 && - user.powerLevel != 50) { - items.add( - PopupMenuItem( - child: Text(L10n.of(context).makeAModerator), value: 'moderator'), - ); - } - if (user.canChangePowerLevel && user.powerLevel != 0) { - items.add( - PopupMenuItem( - child: Text(L10n.of(context).revokeAllPermissions), value: 'user'), - ); - } - if (user.canKick) { - items.add( - PopupMenuItem( - child: Text(L10n.of(context).kickFromChat), value: 'kick'), - ); - } - if (user.canBan && user.membership != Membership.ban) { - items.add( - PopupMenuItem(child: Text(L10n.of(context).banFromChat), value: 'ban'), - ); - } else if (user.canBan && user.membership == Membership.ban) { - items.add( - PopupMenuItem( - child: Text(L10n.of(context).removeExile), value: 'unban'), - ); - } - return PopupMenuButton( - onSelected: (action) => participantAction(context, action), - itemBuilder: (c) => items, - child: ListTile( - title: Row( - children: [ - Text(user.calcDisplayname()), - permissionBatch.isEmpty - ? Container() - : Container( - padding: EdgeInsets.all(4), - margin: EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: Theme.of(context).secondaryHeaderColor, - borderRadius: BorderRadius.circular(8), - ), - child: Center(child: Text(permissionBatch)), - ), - membershipBatch[user.membership].isEmpty - ? Container() - : Container( - padding: EdgeInsets.all(4), - margin: EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: Theme.of(context).secondaryHeaderColor, - borderRadius: BorderRadius.circular(8), - ), - child: - Center(child: Text(membershipBatch[user.membership])), - ), - ], + return ListTile( + onTap: () => showModalBottomSheet( + context: context, + builder: (context) => UserBottomSheet( + user: user, ), - subtitle: Text(user.id), - leading: Avatar(user.avatarUrl, user.calcDisplayname()), ), + title: Row( + children: [ + Text(user.calcDisplayname()), + permissionBatch.isEmpty + ? Container() + : Container( + padding: EdgeInsets.all(4), + margin: EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: Theme.of(context).secondaryHeaderColor, + borderRadius: BorderRadius.circular(8), + ), + child: Center(child: Text(permissionBatch)), + ), + membershipBatch[user.membership].isEmpty + ? Container() + : Container( + padding: EdgeInsets.all(4), + margin: EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: Theme.of(context).secondaryHeaderColor, + borderRadius: BorderRadius.circular(8), + ), + child: Center(child: Text(membershipBatch[user.membership])), + ), + ], + ), + subtitle: Text(user.id), + leading: Avatar(user.avatarUrl, user.calcDisplayname()), ); } } diff --git a/lib/components/list_items/status_list_item.dart b/lib/components/list_items/status_list_item.dart deleted file mode 100644 index 5c8f1b9..0000000 --- a/lib/components/list_items/status_list_item.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:flutter/material.dart'; - -import '../../utils/user_status.dart'; -import '../../views/status_view.dart'; -import '../avatar.dart'; -import '../matrix.dart'; - -class StatusListItem extends StatelessWidget { - final UserStatus status; - - const StatusListItem(this.status, {Key key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final client = Matrix.of(context).client; - return FutureBuilder( - future: client.getProfileFromUserId(status.userId), - builder: (context, snapshot) { - final profile = - snapshot.data ?? Profile(status.userId.localpart, null); - return InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => StatusView( - status: status, - avatarUrl: profile.avatarUrl, - displayname: profile.displayname, - ), - ), - ), - child: Container( - width: 76, - child: Column( - children: [ - SizedBox(height: 10), - Container( - child: Stack( - children: [ - Avatar(profile.avatarUrl, profile.displayname), - Positioned( - bottom: 0, - right: 0, - child: Container( - width: 10, - height: 10, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: Colors.green, - ), - ), - ), - ], - ), - decoration: BoxDecoration( - border: Border.all( - width: 1, - color: Theme.of(context).primaryColor, - ), - borderRadius: BorderRadius.circular(80), - ), - padding: EdgeInsets.all(2), - ), - Padding( - padding: - const EdgeInsets.only(left: 6.0, top: 0.0, right: 6.0), - child: Text( - profile.displayname.trim().split(' ').first, - overflow: TextOverflow.clip, - maxLines: 1, - style: TextStyle( - color: Theme.of(context).textTheme.bodyText2.color, - fontSize: 13, - ), - ), - ), - ], - ), - ), - ); - }); - } -} diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 241e072..c22fa16 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -82,8 +82,7 @@ class MatrixState extends State { void clean() async { if (!kIsWeb) return; - final storage = await getLocalStorage(); - await storage.deleteItem(widget.clientName); + await store.deleteItem(widget.clientName); } void _initWithStore() async { @@ -93,7 +92,6 @@ class MatrixState extends State { await client.connect(); final firstLoginState = await initLoginState; if (firstLoginState == LoginState.logged) { - _cleanUpUserStatus(userStatuses); if (PlatformInfos.isMobile) { await FirebaseController.setupFirebase( this, @@ -124,7 +122,6 @@ class MatrixState extends State { StreamSubscription onNotification; StreamSubscription onFocusSub; StreamSubscription onBlurSub; - StreamSubscription onPresenceSub; void onJitsiCall(EventUpdate eventUpdate) { final event = Event.fromJson( @@ -247,12 +244,9 @@ class MatrixState extends State { importantStateEvents: { 'im.ponies.room_emotes', // we want emotes to work properly }); - onPresenceSub ??= client.onPresence.stream - .where((p) => p.isUserStatus) - .listen(_storeUserStatus); onJitsiCallSub ??= client.onEvent.stream .where((e) => - e.type == 'timeline' && + e.type == EventUpdateType.timeline && e.eventType == 'm.room.message' && e.content['content']['msgtype'] == Matrix.callNamespace && e.content['sender'] != client.userID) @@ -331,7 +325,7 @@ class MatrixState extends State { html.Notification.requestPermission(); onNotification ??= client.onEvent.stream .where((e) => - e.type == 'timeline' && + e.type == EventUpdateType.timeline && [EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted] .contains(e.eventType) && e.content['sender'] != client.userID) @@ -341,64 +335,11 @@ class MatrixState extends State { super.initState(); } - List get userStatuses { - try { - return (client.accountData[userStatusesType].content['user_statuses'] - as List) - .map((json) => UserStatus.fromJson(json)) - .toList(); - } catch (_) {} - return []; - } - - void _storeUserStatus(Presence presence) { - final tmpUserStatuses = List.from(userStatuses); - final currentStatusIndex = - userStatuses.indexWhere((u) => u.userId == presence.senderId); - final newUserStatus = UserStatus() - ..receivedAt = DateTime.now().millisecondsSinceEpoch - ..statusMsg = presence.presence.statusMsg - ..userId = presence.senderId; - if (currentStatusIndex == -1) { - tmpUserStatuses.add(newUserStatus); - } else if (tmpUserStatuses[currentStatusIndex].statusMsg != - presence.presence.statusMsg) { - if (presence.presence.statusMsg.trim().isEmpty) { - tmpUserStatuses.removeAt(currentStatusIndex); - } else { - tmpUserStatuses[currentStatusIndex] = newUserStatus; - } - } else { - return; - } - _cleanUpUserStatus(tmpUserStatuses); - } - - void _cleanUpUserStatus(List tmpUserStatuses) { - final now = DateTime.now().millisecondsSinceEpoch; - tmpUserStatuses - .removeWhere((u) => (now - u.receivedAt) > (1000 * 60 * 60 * 24)); - tmpUserStatuses.sort((a, b) => b.receivedAt.compareTo(a.receivedAt)); - if (tmpUserStatuses.length > 40) { - tmpUserStatuses.removeRange(40, tmpUserStatuses.length); - } - if (tmpUserStatuses != userStatuses) { - client.setAccountData( - client.userID, - userStatusesType, - { - 'user_statuses': tmpUserStatuses.map((i) => i.toJson()).toList(), - }, - ); - } - } - @override void dispose() { onRoomKeyRequestSub?.cancel(); onKeyVerificationRequestSub?.cancel(); onJitsiCallSub?.cancel(); - onPresenceSub?.cancel(); onNotification?.cancel(); onFocusSub?.cancel(); onBlurSub?.cancel(); diff --git a/lib/components/theme_switcher.dart b/lib/components/theme_switcher.dart index 5fecdde..967abf0 100644 --- a/lib/components/theme_switcher.dart +++ b/lib/components/theme_switcher.dart @@ -175,7 +175,7 @@ class ThemeSwitcherWidgetState extends State { BuildContext context; Future loadSelection(MatrixState matrix) async { - String item = await matrix.store.getItem('theme') ?? 'system'; + var item = await matrix.store.getItem('theme') ?? 'system'; selectedTheme = Themes.values.firstWhere( (e) => e.toString() == 'Themes.' + item, orElse: () => Themes.system); diff --git a/lib/components/user_bottom_sheet.dart b/lib/components/user_bottom_sheet.dart new file mode 100644 index 0000000..22095ef --- /dev/null +++ b/lib/components/user_bottom_sheet.dart @@ -0,0 +1,188 @@ +import 'dart:math'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; +import 'package:fluffychat/views/chat.dart'; +import 'package:flutter/material.dart'; +import 'content_banner.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../utils/presence_extension.dart'; +import 'dialogs/simple_dialogs.dart'; +import 'matrix.dart'; + +class UserBottomSheet extends StatelessWidget { + final User user; + final Function onMention; + + const UserBottomSheet({Key key, @required this.user, this.onMention}) + : super(key: key); + + void participantAction(BuildContext context, String action) async { + switch (action) { + case 'mention': + Navigator.of(context).pop(); + onMention(); + break; + case 'ban': + if (await SimpleDialogs(context).askConfirmation()) { + await SimpleDialogs(context).tryRequestWithLoadingDialog(user.ban()); + } + break; + case 'unban': + if (await SimpleDialogs(context).askConfirmation()) { + await SimpleDialogs(context) + .tryRequestWithLoadingDialog(user.unban()); + } + break; + case 'kick': + if (await SimpleDialogs(context).askConfirmation()) { + await SimpleDialogs(context).tryRequestWithLoadingDialog(user.kick()); + } + break; + case 'admin': + if (await SimpleDialogs(context).askConfirmation()) { + await SimpleDialogs(context) + .tryRequestWithLoadingDialog(user.setPower(100)); + } + break; + case 'moderator': + if (await SimpleDialogs(context).askConfirmation()) { + await SimpleDialogs(context) + .tryRequestWithLoadingDialog(user.setPower(50)); + } + break; + case 'user': + if (await SimpleDialogs(context).askConfirmation()) { + await SimpleDialogs(context) + .tryRequestWithLoadingDialog(user.setPower(0)); + } + break; + case 'message': + final roomId = await user.startDirectChat(); + await Navigator.of(context).pushAndRemoveUntil( + AppRoute.defaultRoute( + context, + ChatView(roomId), + ), + (Route r) => r.isFirst); + break; + } + } + + @override + Widget build(BuildContext context) { + final presence = Matrix.of(context).client.presences[user.id]; + var items = >[]; + + if (onMention != null) { + items.add( + PopupMenuItem(child: Text(L10n.of(context).mention), value: 'mention'), + ); + } + if (user.id != Matrix.of(context).client.userID) { + items.add( + PopupMenuItem( + child: Text(L10n.of(context).sendAMessage), value: 'message'), + ); + } + if (user.canChangePowerLevel && + user.room.ownPowerLevel == 100 && + user.powerLevel != 100) { + items.add( + PopupMenuItem( + child: Text(L10n.of(context).makeAnAdmin), value: 'admin'), + ); + } + if (user.canChangePowerLevel && + user.room.ownPowerLevel >= 50 && + user.powerLevel != 50) { + items.add( + PopupMenuItem( + child: Text(L10n.of(context).makeAModerator), value: 'moderator'), + ); + } + if (user.canChangePowerLevel && user.powerLevel != 0) { + items.add( + PopupMenuItem( + child: Text(L10n.of(context).revokeAllPermissions), value: 'user'), + ); + } + if (user.canKick) { + items.add( + PopupMenuItem( + child: Text(L10n.of(context).kickFromChat), value: 'kick'), + ); + } + if (user.canBan && user.membership != Membership.ban) { + items.add( + PopupMenuItem(child: Text(L10n.of(context).banFromChat), value: 'ban'), + ); + } else if (user.canBan && user.membership == Membership.ban) { + items.add( + PopupMenuItem( + child: Text(L10n.of(context).removeExile), value: 'unban'), + ); + } + return Center( + child: Container( + width: min(MediaQuery.of(context).size.width, + AdaptivePageLayout.defaultMinWidth * 1.5), + child: SafeArea( + child: Material( + elevation: 4, + child: Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBar( + elevation: 0, + backgroundColor: + Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5), + leading: IconButton( + icon: Icon(Icons.arrow_downward_outlined), + onPressed: Navigator.of(context).pop, + ), + title: Text(user.calcDisplayname()), + actions: [ + if (user.id != Matrix.of(context).client.userID) + PopupMenuButton( + itemBuilder: (_) => items, + onSelected: (action) => + participantAction(context, action), + ), + ], + ), + body: Column( + children: [ + Expanded( + child: ContentBanner( + user.avatarUrl, + defaultIcon: Icons.person_outline, + ), + ), + ListTile( + title: Text(L10n.of(context).username), + subtitle: Text(user.id), + trailing: Icon(Icons.share), + onTap: () => FluffyShare.share(user.id, context), + ), + if (presence != null) + ListTile( + title: Text(presence.getLocalizedStatusMessage(context)), + subtitle: + Text(presence.getLocalizedLastActiveAgo(context)), + trailing: Icon(Icons.circle, + color: presence.presence.currentlyActive + ? Colors.green + : Colors.grey), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index eeb353c..5636cc4 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -920,6 +920,11 @@ "type": "text", "placeholders": {} }, + "mention": "Mention", + "@mention": { + "type": "text", + "placeholders": {} + }, "messageWillBeRemovedWarning": "Message will be removed for all participants", "@messageWillBeRemovedWarning": { "type": "text", @@ -1017,6 +1022,21 @@ "type": "text", "placeholders": {} }, + "online": "Online", + "@online": { + "type": "text", + "placeholders": {} + }, + "offline": "Offline", + "@offline": { + "type": "text", + "placeholders": {} + }, + "unavailable": "Unavailable", + "@unavailable": { + "type": "text", + "placeholders": {} + }, "onlineKeyBackupEnabled": "Online Key Backup is enabled", "@onlineKeyBackupEnabled": { "type": "text", diff --git a/lib/l10n/intl_eo.arb b/lib/l10n/intl_eo.arb new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/lib/l10n/intl_eo.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index c5cbc6f..089134f 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -461,5 +461,122 @@ "@askSSSSCache": { "type": "text", "placeholders": {} + }, + "enterAUsername": "Inserisci un username", + "@enterAUsername": { + "type": "text", + "placeholders": {} + }, + "enterAGroupName": "Inserisci un nome del gruppo", + "@enterAGroupName": { + "type": "text", + "placeholders": {} + }, + "endedTheCall": "{senderName} è entrato in chiamata", + "@endedTheCall": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "end2endEncryptionSettings": "Impostazioni crittografia end-to-end", + "@end2endEncryptionSettings": { + "type": "text", + "placeholders": {} + }, + "encryptionNotEnabled": "Crittografia non abilitata", + "@encryptionNotEnabled": { + "type": "text", + "placeholders": {} + }, + "encryptionAlgorithm": "Algoritmo crittografia", + "@encryptionAlgorithm": { + "type": "text", + "placeholders": {} + }, + "encryption": "Crittografia", + "@encryption": { + "type": "text", + "placeholders": {} + }, + "enableEncryptionWarning": "Non potrai disabilitare la crittografia in futuro. Sei sicuro?", + "@enableEncryptionWarning": { + "type": "text", + "placeholders": {} + }, + "enableEmotesGlobally": "Abilita i pacchetti emotes globalmente", + "@enableEmotesGlobally": { + "type": "text", + "placeholders": {} + }, + "emptyChat": "Chat vuota", + "@emptyChat": { + "type": "text", + "placeholders": {} + }, + "emotePacks": "Pacchetti emotes della stanza", + "@emotePacks": { + "type": "text", + "placeholders": {} + }, + "emoteInvalid": "Shortcode emote invalido!", + "@emoteInvalid": { + "type": "text", + "placeholders": {} + }, + "emoteExists": "L'emote già esiste!", + "@emoteExists": { + "type": "text", + "placeholders": {} + }, + "emoteWarnNeedToPick": "Devi scegliere uno shortcode emote e aggiungere un immagine!", + "@emoteWarnNeedToPick": { + "type": "text", + "placeholders": {} + }, + "emoteShortcode": "Shortcode Emotes", + "@emoteShortcode": { + "type": "text", + "placeholders": {} + }, + "emoteSettings": "Impostazioni Emotes", + "@emoteSettings": { + "type": "text", + "placeholders": {} + }, + "editDisplayname": "Modifica nominativo", + "@editDisplayname": { + "type": "text", + "placeholders": {} + }, + "downloadFile": "Scarica file", + "@downloadFile": { + "type": "text", + "placeholders": {} + }, + "displaynameHasBeenChanged": "Il nominativo è stato cambiato", + "@displaynameHasBeenChanged": { + "type": "text", + "placeholders": {} + }, + "discardPicture": "Rimuovi immagine", + "@discardPicture": { + "type": "text", + "placeholders": {} + }, + "devices": "Dispositivi", + "@devices": { + "type": "text", + "placeholders": {} + }, + "device": "Dispositivo", + "@device": { + "type": "text", + "placeholders": {} + }, + "deny": "Declina", + "@deny": { + "type": "text", + "placeholders": {} } } diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 3bae65a..305877d 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1733,7 +1733,7 @@ "type": "text", "placeholders": {} }, - "deactivateAccountWarning": "Это деактивирует вашу учётную запись пользователя. Это не может быть отменено! Вы уверены?", + "deactivateAccountWarning": "Это деактивирует вашу учётную запись пользователя. Данное действие не может быть отменено! Вы уверены?", "@deactivateAccountWarning": { "type": "text", "placeholders": {} @@ -1743,12 +1743,12 @@ "type": "text", "placeholders": {} }, - "enableEmotesGlobally": "Включить набор эмоджи глобально", + "enableEmotesGlobally": "Включить набор эмодзи глобально", "@enableEmotesGlobally": { "type": "text", "placeholders": {} }, - "emotePacks": "Наборы эмоджи для комнаты", + "emotePacks": "Наборы эмодзи для комнаты", "@emotePacks": { "type": "text", "placeholders": {} diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index b9fd174..92ba185 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -137,7 +137,7 @@ "targetName": {} } }, - "blockDevice": "Cihazı Engelle", + "blockDevice": "Aygıtı Engelle", "@blockDevice": { "type": "text", "placeholders": {} @@ -323,12 +323,12 @@ "type": "text", "placeholders": {} }, - "compareEmojiMatch": "Karşılaştırın ve aşağıdaki emojilerin diğer cihazdakilerle eşleştiğinden emin olun:", + "compareEmojiMatch": "Karşılaştırın ve aşağıdaki emojilerin diğer aygıttaki emojilerle eşleştiğinden emin olun:", "@compareEmojiMatch": { "type": "text", "placeholders": {} }, - "compareNumbersMatch": "Karşılaştırın ve aşağıdaki numaraların diğer cihazdakilerle eşleştiğinden emin olun:", + "compareNumbersMatch": "Karşılaştırın ve aşağıdaki numaraların diğer aygıttaki numaralarla eşleştiğinden emin olun:", "@compareNumbersMatch": { "type": "text", "placeholders": {} @@ -474,12 +474,12 @@ "type": "text", "placeholders": {} }, - "device": "Cihaz", + "device": "Aygıt", "@device": { "type": "text", "placeholders": {} }, - "devices": "Cihazlar", + "devices": "Aygıtlar", "@devices": { "type": "text", "placeholders": {} @@ -727,7 +727,7 @@ "link": {} } }, - "isDeviceKeyCorrect": "Aşağıdaki cihaz anahtarı doğru mu?", + "isDeviceKeyCorrect": "Aşağıdaki aygıt anahtarı doğru mu?", "@isDeviceKeyCorrect": { "type": "text", "placeholders": {} @@ -983,7 +983,7 @@ "type": "text", "placeholders": {} }, - "participatingUserDevices": "Katılan kullanıcı cihazları", + "participatingUserDevices": "Katılan kullanıcı aygıtları", "@participatingUserDevices": { "type": "text", "placeholders": {} @@ -1069,7 +1069,7 @@ "type": "text", "placeholders": {} }, - "removeAllOtherDevices": "Diğer tüm cihazları kaldır", + "removeAllOtherDevices": "Diğer tüm aygıtları kaldır", "@removeAllOtherDevices": { "type": "text", "placeholders": {} @@ -1081,7 +1081,7 @@ "username": {} } }, - "removeDevice": "Cihazı kaldır", + "removeDevice": "Aygıtı kaldır", "@removeDevice": { "type": "text", "placeholders": {} @@ -1355,12 +1355,12 @@ "targetName": {} } }, - "unblockDevice": "Cihazın Engellemesini Kaldır", + "unblockDevice": "Aygıtın Engellemesini Kaldır", "@unblockDevice": { "type": "text", "placeholders": {} }, - "unknownDevice": "Bilinmeyen cihaz", + "unknownDevice": "Bilinmeyen aygıt", "@unknownDevice": { "type": "text", "placeholders": {} @@ -1718,7 +1718,7 @@ "type": "text", "placeholders": {} }, - "changeDeviceName": "Cihaz adını değiştir", + "changeDeviceName": "Aygıt adını değiştir", "@changeDeviceName": { "type": "text", "placeholders": {} diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb new file mode 100644 index 0000000..a8a59ba --- /dev/null +++ b/lib/l10n/intl_vi.arb @@ -0,0 +1,98 @@ +{ + "blockDevice": "Thiết bị bị chặn", + "@blockDevice": { + "type": "text", + "placeholders": {} + }, + "askSSSSCache": "Vui lòng nhập cụm mật khẩu hoặc khóa khôi phục để lưu khóa vào bộ nhớ cache.", + "@askSSSSCache": { + "type": "text", + "placeholders": {} + }, + "areYouSure": "Bạn chắc chứ?", + "@areYouSure": { + "type": "text", + "placeholders": {} + }, + "areGuestsAllowedToJoin": "Khách vãng lai có được tham gia không", + "@areGuestsAllowedToJoin": { + "type": "text", + "placeholders": {} + }, + "archivedRoom": "Phòng hội thảo đã lưu trữ", + "@archivedRoom": { + "type": "text", + "placeholders": {} + }, + "archive": "Lưu trữ", + "@archive": { + "type": "text", + "placeholders": {} + }, + "anyoneCanJoin": "Mọi người đều có thể gia nhập", + "@anyoneCanJoin": { + "type": "text", + "placeholders": {} + }, + "answeredTheCall": "{senderName} đã trả lời cuộc gọi", + "@answeredTheCall": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "alreadyHaveAnAccount": "Bạn đã có tài khoản?", + "@alreadyHaveAnAccount": { + "type": "text", + "placeholders": {} + }, + "alias": "bí danh", + "@alias": { + "type": "text", + "placeholders": {} + }, + "admin": "Quản trị viên", + "@admin": { + "type": "text", + "placeholders": {} + }, + "addGroupDescription": "Thêm mô tả cho nhóm", + "@addGroupDescription": { + "type": "text", + "placeholders": {} + }, + "activatedEndToEndEncryption": "{username} đã kích hoạt mã hóa đầu cuối 2 chiều", + "@activatedEndToEndEncryption": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "accountInformation": "Thông tin tài khoản", + "@accountInformation": { + "type": "text", + "placeholders": {} + }, + "account": "Tài khoản", + "@account": { + "type": "text", + "placeholders": {} + }, + "acceptedTheInvitation": "{username} đã đồng ý lời mời", + "@acceptedTheInvitation": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "accept": "Đồng ý", + "@accept": { + "type": "text", + "placeholders": {} + }, + "about": "Giới thiệu", + "@about": { + "type": "text", + "placeholders": {} + } +} diff --git a/lib/utils/famedlysdk_store.dart b/lib/utils/famedlysdk_store.dart index f828e4c..9e671be 100644 --- a/lib/utils/famedlysdk_store.dart +++ b/lib/utils/famedlysdk_store.dart @@ -1,27 +1,12 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:core'; - -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import '../famedlysdk.dart'; +import './platform_infos.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:localstorage/localstorage.dart'; -import 'package:olm/olm.dart' as olm; // needed for migration import 'package:path_provider/path_provider.dart'; -import 'package:random_string/random_string.dart'; - +import 'dart:async'; +import 'dart:core'; import './database/shared.dart'; -import 'platform_infos.dart'; - -Future getLocalStorage() async { - final directory = PlatformInfos.isBetaDesktop - ? await getApplicationSupportDirectory() - : (PlatformInfos.isWeb ? null : await getApplicationDocumentsDirectory()); - final localStorage = LocalStorage('LocalStorage', directory?.path); - await localStorage.ready; - return localStorage; -} +import 'package:random_string/random_string.dart'; Future getDatabase(Client client) async { while (_generateDatabaseLock) { @@ -32,9 +17,9 @@ Future getDatabase(Client client) async { if (_db != null) return _db; final store = Store(); var password = await store.getItem('database-password'); - var needMigration = false; + var newPassword = false; if (password == null || password.isEmpty) { - needMigration = true; + newPassword = true; password = randomString(255); } _db = await constructDb( @@ -42,11 +27,7 @@ Future getDatabase(Client client) async { filename: 'moor.sqlite', password: password, ); - // Check if database is open: - debugPrint((await _db.customSelect('SELECT 1').get()).toString()); - if (needMigration) { - debugPrint('[Moor] Start migration'); - await migrate(client.clientName, _db, store); + if (newPassword) { await store.setItem('database-password', password); } return _db; @@ -58,239 +39,54 @@ Future getDatabase(Client client) async { Database _db; bool _generateDatabaseLock = false; -Future migrate(String clientName, Database db, Store store) async { - debugPrint('[Store] attempting old migration to moor...'); - final oldKeys = await store.getAllItems(); - if (oldKeys == null || oldKeys.isEmpty) { - debugPrint('[Store] empty store!'); - return; // we are done! - } - final credentialsStr = oldKeys[clientName]; - if (credentialsStr == null || credentialsStr.isEmpty) { - debugPrint('[Store] no credentials found!'); - return; // no credentials - } - final Map credentials = json.decode(credentialsStr); - if (!credentials.containsKey('homeserver') || - !credentials.containsKey('token') || - !credentials.containsKey('userID')) { - debugPrint('[Store] invalid credentials!'); - return; // invalid old store, we are done, too! - } - var clientId = 0; - final oldClient = await db.getClient(clientName); - if (oldClient == null) { - clientId = await db.insertClient( - clientName, - credentials['homeserver'], - credentials['token'], - credentials['userID'], - credentials['deviceID'], - credentials['deviceName'], - null, - credentials['olmAccount'], - ); - } else { - clientId = oldClient.clientId; - await db.updateClient( - credentials['homeserver'], - credentials['token'], - credentials['userID'], - credentials['deviceID'], - credentials['deviceName'], - null, - credentials['olmAccount'], - clientId, - ); - } - await db.clearCache(clientId); - debugPrint('[Store] Inserted/updated client, clientId = ${clientId}'); - await db.transaction(() async { - // alright, we stored / updated the client and have the account ID, time to import everything else! - // user_device_keys and user_device_keys_key - debugPrint('[Store] Migrating user device keys...'); - final deviceKeysListString = oldKeys['${clientName}.user_device_keys']; - if (deviceKeysListString != null && deviceKeysListString.isNotEmpty) { - Map rawUserDeviceKeys = - json.decode(deviceKeysListString); - for (final entry in rawUserDeviceKeys.entries) { - final map = entry.value; - await db.storeUserDeviceKeysInfo( - clientId, map['user_id'], map['outdated']); - for (final rawKey in map['device_keys'].entries) { - final jsonVaue = rawKey.value; - await db.storeUserDeviceKey( - clientId, - jsonVaue['user_id'], - jsonVaue['device_id'], - json.encode(jsonVaue), - jsonVaue['verified'], - jsonVaue['blocked']); - } - } - } - for (final entry in oldKeys.entries) { - final key = entry.key; - final value = entry.value; - if (value == null || value.isEmpty) { - continue; - } - // olm_sessions - final olmSessionsMatch = - RegExp(r'^\/clients\/([^\/]+)\/olm-sessions$').firstMatch(key); - if (olmSessionsMatch != null) { - if (olmSessionsMatch[1] != credentials['deviceID']) { - continue; - } - debugPrint('[Store] migrating olm sessions...'); - final identityKey = json.decode(value); - for (final olmKey in identityKey.entries) { - final identKey = olmKey.key; - final sessions = olmKey.value; - for (final pickle in sessions) { - var sess = olm.Session(); - sess.unpickle(credentials['userID'], pickle); - await db.storeOlmSession( - clientId, identKey, sess.session_id(), pickle, null); - sess?.free(); - } - } - } - // outbound_group_sessions - final outboundGroupSessionsMatch = RegExp( - r'^\/clients\/([^\/]+)\/rooms\/([^\/]+)\/outbound_group_session$') - .firstMatch(key); - if (outboundGroupSessionsMatch != null) { - if (outboundGroupSessionsMatch[1] != credentials['deviceID']) { - continue; - } - final pickle = value; - final roomId = outboundGroupSessionsMatch[2]; - debugPrint( - '[Store] Migrating outbound group sessions for room ${roomId}...'); - final devicesString = oldKeys[ - '/clients/${outboundGroupSessionsMatch[1]}/rooms/${roomId}/outbound_group_session_devices']; - var devices = []; - if (devicesString != null) { - devices = List.from(json.decode(devicesString)); - } - await db.storeOutboundGroupSession( - clientId, - roomId, - pickle, - json.encode(devices), - DateTime.now().millisecondsSinceEpoch, - 0, - ); - } - // session_keys - final sessionKeysMatch = - RegExp(r'^\/clients\/([^\/]+)\/rooms\/([^\/]+)\/session_keys$') - .firstMatch(key); - if (sessionKeysMatch != null) { - if (sessionKeysMatch[1] != credentials['deviceID']) { - continue; - } - final roomId = sessionKeysMatch[2]; - debugPrint('[Store] Migrating session keys for room ${roomId}...'); - final map = json.decode(value); - for (final entry in map.entries) { - await db.storeInboundGroupSession( - clientId, - roomId, - entry.key, - entry.value['inboundGroupSession'], - json.encode(entry.value['content']), - json.encode(entry.value['indexes']), - null, - null); - } - } - } - }); -} - -// see https://github.com/mogol/flutter_secure_storage/issues/161#issuecomment-704578453 -class AsyncMutex { - Completer _completer; - - Future lock() async { - while (_completer != null) { - await _completer.future; - } - - _completer = Completer(); - } - - void unlock() { - assert(_completer != null); - final completer = _completer; - _completer = null; - completer.complete(); - } -} - class Store { - final LocalStorage storage; + LocalStorage storage; final FlutterSecureStorage secureStorage; - static final _mutex = AsyncMutex(); Store() - : storage = LocalStorage('LocalStorage'), - secureStorage = PlatformInfos.isMobile ? FlutterSecureStorage() : null; + : secureStorage = PlatformInfos.isMobile ? FlutterSecureStorage() : null; - Future getItem(String key) async { - if (!PlatformInfos.isMobile) { + Future _setupLocalStorage() async { + if (storage == null) { + final directory = PlatformInfos.isBetaDesktop + ? await getApplicationSupportDirectory() + : (PlatformInfos.isWeb + ? null + : await getApplicationDocumentsDirectory()); + storage = LocalStorage('LocalStorage', directory?.path); await storage.ready; + } + } + + Future getItem(String key) async { + if (!PlatformInfos.isMobile) { + await _setupLocalStorage(); try { - return await storage.getItem(key); + return await storage.getItem(key)?.toString(); } catch (_) { return null; } } try { - await _mutex.lock(); return await secureStorage.read(key: key); } catch (_) { return null; - } finally { - _mutex.unlock(); } } Future setItem(String key, String value) async { if (!PlatformInfos.isMobile) { - await storage.ready; + await _setupLocalStorage(); return await storage.setItem(key, value); } - if (value == null) { - return await secureStorage.delete(key: key); - } else { - try { - await _mutex.lock(); - return await secureStorage.write(key: key, value: value); - } finally { - _mutex.unlock(); - } - } + return await secureStorage.write(key: key, value: value); } - Future> getAllItems() async { + Future deleteItem(String key) async { if (!PlatformInfos.isMobile) { - try { - final rawStorage = await getLocalstorage('LocalStorage'); - return json.decode(rawStorage); - } catch (_) { - return {}; - } - } - try { - await _mutex.lock(); - return await secureStorage.readAll(); - } catch (_) { - return {}; - } finally { - _mutex.unlock(); + await _setupLocalStorage(); + return await storage.deleteItem(key); } + return await secureStorage.delete(key: key); } } diff --git a/lib/utils/firebase_controller.dart b/lib/utils/firebase_controller.dart index 37b7a69..3aab786 100644 --- a/lib/utils/firebase_controller.dart +++ b/lib/utils/firebase_controller.dart @@ -130,7 +130,9 @@ abstract class FirebaseController { var initializationSettingsIOS = IOSInitializationSettings( onDidReceiveLocalNotification: (i, a, b, c) => null); var initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, iOS: initializationSettingsIOS); + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + ); await _flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: goToRoom); @@ -266,8 +268,9 @@ abstract class FirebaseController { ticker: i18n.newMessageInFluffyChat); var iOSPlatformChannelSpecifics = IOSNotificationDetails(); var platformChannelSpecifics = NotificationDetails( - android: androidPlatformChannelSpecifics, - iOS: iOSPlatformChannelSpecifics); + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics, + ); await _flutterLocalNotificationsPlugin.show( 0, room.getLocalizedDisplayname(MatrixLocals(i18n)), @@ -297,8 +300,9 @@ abstract class FirebaseController { AndroidInitializationSettings('notifications_icon'); var initializationSettingsIOS = IOSInitializationSettings(); var initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, - iOS: initializationSettingsIOS); + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + ); await flutterLocalNotificationsPlugin.initialize(initializationSettings); // FIXME unable to init without context currently https://github.com/flutter/flutter/issues/67092 @@ -324,8 +328,9 @@ abstract class FirebaseController { importance: Importance.max, priority: Priority.high); var iOSPlatformChannelSpecifics = IOSNotificationDetails(); var platformChannelSpecifics = NotificationDetails( - android: androidPlatformChannelSpecifics, - iOS: iOSPlatformChannelSpecifics); + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics, + ); final title = l10n.unreadChats(unread.toString()); await flutterLocalNotificationsPlugin.show( 1, title, l10n.openAppToReadMessages, platformChannelSpecifics, diff --git a/lib/utils/fluffy_share.dart b/lib/utils/fluffy_share.dart new file mode 100644 index 0000000..88409a3 --- /dev/null +++ b/lib/utils/fluffy_share.dart @@ -0,0 +1,19 @@ +import 'package:bot_toast/bot_toast.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:share/share.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +abstract class FluffyShare { + static Future share(String text, BuildContext context) async { + if (PlatformInfos.isMobile) { + return Share.share(text); + } + await Clipboard.setData( + ClipboardData(text: text), + ); + BotToast.showText(text: L10n.of(context).copiedToClipboard); + return; + } +} diff --git a/lib/utils/matrix_file_extension.dart b/lib/utils/matrix_file_extension.dart index 2f1dc44..d7daef3 100644 --- a/lib/utils/matrix_file_extension.dart +++ b/lib/utils/matrix_file_extension.dart @@ -1,11 +1,14 @@ import 'dart:io'; import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/foundation.dart'; import 'package:open_file/open_file.dart'; import 'package:path_provider/path_provider.dart'; import 'package:universal_html/prefer_universal/html.dart' as html; import 'package:mime_type/mime_type.dart'; +import 'package:downloads_path_provider_28/downloads_path_provider_28.dart'; +import 'package:permission_handler/permission_handler.dart'; extension MatrixFileExtension on MatrixFile { void open() async { @@ -24,9 +27,12 @@ extension MatrixFileExtension on MatrixFile { element.click(); element.remove(); } else { - final downloadsDir = Platform.isAndroid - ? (await getExternalStorageDirectory()) - : (await getApplicationDocumentsDirectory()); + if (!(await Permission.storage.request()).isGranted) return; + final downloadsDir = PlatformInfos.isDesktop + ? (await getDownloadsDirectory()) + : Platform.isAndroid + ? (await DownloadsPathProvider.downloadsDirectory) + : (await getApplicationDocumentsDirectory()); final file = File(downloadsDir.path + '/' + name.split('/').last); file.writeAsBytesSync(bytes); diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index 5473a8c..3bc4898 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -11,5 +11,8 @@ abstract class PlatformInfos { static bool get isBetaDesktop => !kIsWeb && (Platform.isWindows || Platform.isLinux); + static bool get isDesktop => + !kIsWeb && (Platform.isLinux || Platform.isWindows || Platform.isMacOS); + static bool get usesTouchscreen => !isMobile; } diff --git a/lib/utils/presence_extension.dart b/lib/utils/presence_extension.dart index bfbd032..50203f8 100644 --- a/lib/utils/presence_extension.dart +++ b/lib/utils/presence_extension.dart @@ -4,21 +4,37 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'date_time_extension.dart'; +extension on PresenceType { + String getLocalized(BuildContext context) { + switch (this) { + case PresenceType.online: + return L10n.of(context).online; + case PresenceType.unavailable: + return L10n.of(context).unavailable; + case PresenceType.offline: + default: + return L10n.of(context).offline; + } + } +} + extension PresenceExtension on Presence { - bool get isUserStatus => presence?.statusMsg?.isNotEmpty ?? false; + String getLocalizedLastActiveAgo(BuildContext context) { + if (presence.lastActiveAgo != null && presence.lastActiveAgo != 0) { + return L10n.of(context).lastActiveAgo(DateTime.fromMillisecondsSinceEpoch( + DateTime.now().millisecondsSinceEpoch - presence.lastActiveAgo) + .localizedTimeShort(context)); + } + return L10n.of(context).lastSeenLongTimeAgo; + } String getLocalizedStatusMessage(BuildContext context) { if (presence.statusMsg?.isNotEmpty ?? false) { return presence.statusMsg; } - if (presence.lastActiveAgo != null ?? presence.lastActiveAgo != 0) { - return L10n.of(context).lastActiveAgo( - DateTime.fromMillisecondsSinceEpoch(presence.lastActiveAgo) - .localizedTimeShort(context)); - } - if (presence.currentlyActive) { + if (presence.currentlyActive ?? false) { return L10n.of(context).currentlyActive; } - return L10n.of(context).lastSeenLongTimeAgo; + return presence.presence.getLocalized(context); } } diff --git a/lib/utils/user_status.dart b/lib/utils/user_status.dart deleted file mode 100644 index dadfb3f..0000000 --- a/lib/utils/user_status.dart +++ /dev/null @@ -1,21 +0,0 @@ -class UserStatus { - String statusMsg; - String userId; - int receivedAt; - - UserStatus(); - - UserStatus.fromJson(Map json) { - statusMsg = json['status_msg']; - userId = json['user_id']; - receivedAt = json['received_at']; - } - - Map toJson() { - final data = {}; - data['status_msg'] = statusMsg; - data['user_id'] = userId; - data['received_at'] = receivedAt; - return data; - } -} diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 58117d5..1028cd2 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -25,6 +25,7 @@ import '../components/encryption_button.dart'; import '../components/input_bar.dart'; import '../components/list_items/message.dart'; import '../components/matrix.dart'; +import '../components/user_bottom_sheet.dart'; import '../components/reply_content.dart'; import '../config/app_emojis.dart'; import '../utils/app_route.dart'; @@ -604,16 +605,22 @@ class _ChatState extends State<_Chat> { return ListTile( leading: Avatar(room.avatar, room.displayname), contentPadding: EdgeInsets.zero, - onTap: room.isDirectChat && room.directChatPresence == null - ? null - : room.isDirectChat - ? null - : () => Navigator.of(context).push( - AppRoute.defaultRoute( - context, - ChatDetails(room), - ), - ), + onTap: room.isDirectChat + ? () => showModalBottomSheet( + context: context, + builder: (context) => UserBottomSheet( + user: room + .getUserByMXIDSync(room.directChatMatrixID), + onMention: () => sendController.text += + ' ${room.directChatMatrixID}', + ), + ) + : () => Navigator.of(context).push( + AppRoute.defaultRoute( + context, + ChatDetails(room), + ), + ), title: Text( room.getLocalizedDisplayname( MatrixLocals(L10n.of(context))), @@ -801,10 +808,17 @@ class _ChatState extends State<_Chat> { onSwipe: (direction) => _handleSwipe( direction, filteredEvents[i - 1]), child: Message(filteredEvents[i - 1], - onAvatarTab: (Event event) { - sendController.text += - ' ${event.senderId}'; - }, + onAvatarTab: (Event event) => + showModalBottomSheet( + context: context, + builder: (context) => + UserBottomSheet( + user: event.sender, + onMention: () => + sendController.text += + ' ${event.senderId}', + ), + ), onSelect: (Event event) { if (!event.redacted) { if (selectedEvents diff --git a/lib/views/chat_list.dart b/lib/views/chat_list.dart index fde309b..24e5aa9 100644 --- a/lib/views/chat_list.dart +++ b/lib/views/chat_list.dart @@ -7,7 +7,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -import 'package:share/share.dart'; import '../components/adaptive_page_layout.dart'; import '../components/avatar.dart'; @@ -19,6 +18,7 @@ import '../components/list_items/status_list_item.dart'; import '../components/matrix.dart'; import '../utils/app_route.dart'; import '../utils/matrix_file_extension.dart'; +import '../utils/fluffy_share.dart'; import '../utils/platform_infos.dart'; import '../utils/url_launcher.dart'; import 'archive.dart'; @@ -198,29 +198,23 @@ class _ChatListState extends State { ); } - void _setStatus(BuildContext context, {bool fromDrawer = false}) async { - if (fromDrawer) Navigator.of(context).pop(); - final ownProfile = await SimpleDialogs(context) - .tryRequestWithLoadingDialog(Matrix.of(context).client.ownProfile); - String composeText; - if (Matrix.of(context).shareContent != null && - Matrix.of(context).shareContent['msgtype'] == 'm.text') { - composeText = Matrix.of(context).shareContent['body']; - Matrix.of(context).shareContent = null; - } - if (ownProfile is Profile) { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => StatusView( - composeMode: true, - avatarUrl: ownProfile.avatarUrl, - displayname: ownProfile.displayname ?? - Matrix.of(context).client.userID.localpart, - composeText: composeText, - ), - ), - ); - } + void _setStatus(BuildContext context) async { + Navigator.of(context).pop(); + final statusMsg = await SimpleDialogs(context).enterText( + titleText: L10n.of(context).setStatus, + labelText: L10n.of(context).setStatus, + hintText: L10n.of(context).statusExampleMessage, + multiLine: true, + ); + if (statusMsg?.isEmpty ?? true) return; + final client = Matrix.of(context).client; + await SimpleDialogs(context).tryRequestWithLoadingDialog( + client.sendPresence( + client.userID, + PresenceType.online, + statusMsg: statusMsg, + ), + ); return; } @@ -302,8 +296,7 @@ class _ChatListState extends State { ListTile( leading: Icon(Icons.edit), title: Text(L10n.of(context).setStatus), - onTap: () => - _setStatus(context, fromDrawer: true), + onTap: () => _setStatus(context), ), Divider(height: 1), ListTile( @@ -338,9 +331,11 @@ class _ChatListState extends State { title: Text(L10n.of(context).inviteContact), onTap: () { Navigator.of(context).pop(); - Share.share(L10n.of(context).inviteText( - Matrix.of(context).client.userID, - 'https://matrix.to/#/${Matrix.of(context).client.userID}')); + FluffyShare.share( + L10n.of(context).inviteText( + Matrix.of(context).client.userID, + 'https://matrix.to/#/${Matrix.of(context).client.userID}'), + context); }, ), ], @@ -422,31 +417,14 @@ class _ChatListState extends State { ), floatingActionButton: AdaptivePageLayout.columnMode(context) ? null - : Column( - mainAxisSize: MainAxisSize.min, - children: [ - FloatingActionButton( - heroTag: null, - child: Icon( - Icons.edit, - color: Theme.of(context).primaryColor, - ), - elevation: 1, - backgroundColor: - Theme.of(context).secondaryHeaderColor, - onPressed: () => _setStatus(context), - ), - SizedBox(height: 16.0), - FloatingActionButton( - child: Icon(Icons.add), - backgroundColor: Theme.of(context).primaryColor, - onPressed: () => Navigator.of(context) - .pushAndRemoveUntil( - AppRoute.defaultRoute( - context, NewPrivateChatView()), - (r) => r.isFirst), - ), - ], + : FloatingActionButton( + child: Icon(Icons.add), + backgroundColor: Theme.of(context).primaryColor, + onPressed: () => Navigator.of(context) + .pushAndRemoveUntil( + AppRoute.defaultRoute( + context, NewPrivateChatView()), + (r) => r.isFirst), ), body: Column( children: [ @@ -506,94 +484,28 @@ class _ChatListState extends State { final totalCount = rooms.length + publicRoomsCount; return ListView.separated( - controller: _scrollController, - separatorBuilder: (BuildContext context, - int i) => - i == totalCount - publicRoomsCount - ? ListTile( - title: Text( - L10n.of(context) - .publicRooms + - ':', - style: TextStyle( - fontWeight: - FontWeight.bold, - color: Theme.of(context) - .primaryColor, - ), + controller: _scrollController, + separatorBuilder: (BuildContext context, + int i) => + i == totalCount - publicRoomsCount + ? ListTile( + title: Text( + L10n.of(context) + .publicRooms + + ':', + style: TextStyle( + fontWeight: + FontWeight.bold, + color: Theme.of(context) + .primaryColor, ), - ) - : Container(), - itemCount: totalCount + 1, - itemBuilder: - (BuildContext context, int i) { - if (i == 0) { - final displayPresences = - selectMode != SelectMode.share; - final displayShareStatus = - selectMode == - SelectMode.share && - Matrix.of(context) - .shareContent[ - 'msgtype'] == - 'm.text'; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - AnimatedContainer( - duration: Duration( - milliseconds: 300), - height: displayPresences - ? 78 - : displayShareStatus - ? 56 - : 0, - child: displayPresences - ? ListView.builder( - scrollDirection: - Axis.horizontal, - itemCount: - Matrix.of(context) - .userStatuses - .length, - itemBuilder: (BuildContext - context, - int i) => - StatusListItem(Matrix - .of(context) - .userStatuses[i]), - ) - : displayShareStatus - ? ListTile( - leading: - CircleAvatar( - radius: Avatar - .defaultSize / - 2, - backgroundColor: - Theme.of( - context) - .secondaryHeaderColor, - child: Icon( - Icons.edit, - color: Theme.of( - context) - .primaryColor, - ), - ), - title: Text(L10n.of( - context) - .setStatus), - onTap: () => - _setStatus( - context)) - : null, - ), - ], - ); - } - i--; - return i < rooms.length + ), + ) + : Container(), + itemCount: totalCount, + itemBuilder: (BuildContext context, + int i) => + i < rooms.length ? ChatListItem( rooms[i], selected: _selectedRoomIds @@ -614,8 +526,9 @@ class _ChatListState extends State { ) : PublicRoomListItem( publicRoomsResponse - .chunk[i - rooms.length]); - }); + .chunk[i - rooms.length], + ), + ); } else { return Center( child: CircularProgressIndicator(), diff --git a/lib/views/homeserver_picker.dart b/lib/views/homeserver_picker.dart index f479ec0..2cbf941 100644 --- a/lib/views/homeserver_picker.dart +++ b/lib/views/homeserver_picker.dart @@ -76,6 +76,11 @@ class _HomeserverPickerState extends State { } } + Future checkHomeserver(dynamic homeserver, Client client) async { + await client.checkHomeserver(homeserver); + return true; + } + @override void initState() { super.initState(); diff --git a/lib/views/login.dart b/lib/views/login.dart index e64023e..3923ff2 100644 --- a/lib/views/login.dart +++ b/lib/views/login.dart @@ -123,7 +123,7 @@ class _LoginState extends State { if ((newDomain?.isNotEmpty ?? false) && newDomain != Matrix.of(context).client.homeserver.toString()) { await SimpleDialogs(context).tryRequestWithErrorToast( - Matrix.of(context).client.checkServer(newDomain)); + Matrix.of(context).client.checkHomeserver(newDomain)); setState(() => usernameError = null); } newWellknown = wellKnownInformations; diff --git a/lib/views/new_private_chat.dart b/lib/views/new_private_chat.dart index 4610d0d..a67df4d 100644 --- a/lib/views/new_private_chat.dart +++ b/lib/views/new_private_chat.dart @@ -4,13 +4,13 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/matrix_api.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:share/share.dart'; import '../components/adaptive_page_layout.dart'; import '../components/avatar.dart'; import '../components/dialogs/simple_dialogs.dart'; import '../components/matrix.dart'; import '../utils/app_route.dart'; +import '../utils/fluffy_share.dart'; import 'chat.dart'; import 'chat_list.dart'; @@ -204,9 +204,10 @@ class _NewPrivateChatState extends State<_NewPrivateChat> { Icons.share, size: 16, ), - onTap: () => Share.share(L10n.of(context).inviteText( - Matrix.of(context).client.userID, - 'https://matrix.to/#/${Matrix.of(context).client.userID}')), + onTap: () => FluffyShare.share( + L10n.of(context).inviteText(Matrix.of(context).client.userID, + 'https://matrix.to/#/${Matrix.of(context).client.userID}'), + context), title: Text( '${L10n.of(context).yourOwnUsername}:', style: TextStyle( diff --git a/lib/views/status_view.dart b/lib/views/status_view.dart deleted file mode 100644 index 7e7d17a..0000000 --- a/lib/views/status_view.dart +++ /dev/null @@ -1,186 +0,0 @@ -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix_link_text/link_text.dart'; - -import '../components/avatar.dart'; -import '../components/dialogs/simple_dialogs.dart'; -import '../components/matrix.dart'; -import '../utils/app_route.dart'; -import '../utils/string_color.dart'; -import '../utils/url_launcher.dart'; -import '../utils/user_status.dart'; -import 'chat.dart'; - -class StatusView extends StatelessWidget { - final Uri avatarUrl; - final String displayname; - final UserStatus status; - final bool composeMode; - final String composeText; - final TextEditingController _composeController; - - StatusView({ - this.composeMode = false, - this.status, - this.avatarUrl, - this.displayname, - this.composeText, - Key key, - }) : _composeController = TextEditingController(text: composeText), - super(key: key); - - void _sendMessageAction(BuildContext context) async { - final roomId = await User( - status.userId, - room: Room(id: '', client: Matrix.of(context).client), - ).startDirectChat(); - await Navigator.of(context).pushAndRemoveUntil( - AppRoute.defaultRoute( - context, - ChatView(roomId), - ), - (Route r) => r.isFirst); - } - - void _setStatusAction(BuildContext context) async { - if (_composeController.text.isEmpty) return; - await SimpleDialogs(context).tryRequestWithLoadingDialog( - Matrix.of(context).client.sendPresence( - Matrix.of(context).client.userID, PresenceType.online, - statusMsg: _composeController.text), - ); - await Navigator.of(context).popUntil((Route r) => r.isFirst); - } - - void _removeStatusAction(BuildContext context) async { - final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( - Matrix.of(context).client.sendPresence( - Matrix.of(context).client.userID, - PresenceType.online, - statusMsg: - ' ', // Send this empty String make sure that all other devices will get an update - ), - ); - if (success == false) return; - await Navigator.of(context).popUntil((Route r) => r.isFirst); - } - - @override - Widget build(BuildContext context) { - if (composeMode == false && status == null) { - throw ('If composeMode is null then the presence must be not null!'); - } - final padding = const EdgeInsets.only( - top: 16.0, - right: 16.0, - left: 16.0, - bottom: 64.0, - ); - return Scaffold( - backgroundColor: displayname.color, - extendBody: true, - appBar: AppBar( - titleSpacing: 0.0, - brightness: Brightness.dark, - leading: IconButton( - icon: Icon( - Icons.close, - color: Colors.white, - ), - onPressed: Navigator.of(context).pop, - ), - backgroundColor: Colors.transparent, - elevation: 1, - title: ListTile( - contentPadding: EdgeInsets.zero, - leading: Avatar(avatarUrl, displayname), - title: Text( - displayname, - style: TextStyle(color: Colors.white), - ), - subtitle: Text( - status?.userId ?? Matrix.of(context).client.userID, - maxLines: 1, - style: TextStyle(color: Colors.white), - ), - ), - actions: - !composeMode && status.userId == Matrix.of(context).client.userID - ? [ - IconButton( - icon: Icon(Icons.archive), - onPressed: () => _removeStatusAction(context), - color: Colors.white, - ), - ] - : null, - ), - body: Container( - alignment: Alignment.center, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - displayname.color, - Theme.of(context).primaryColor, - displayname.color, - ], - ), - ), - child: composeMode - ? Padding( - padding: padding, - child: TextField( - controller: _composeController, - autofocus: true, - minLines: 1, - maxLines: 20, - style: TextStyle( - fontSize: 30, - color: Colors.white, - ), - textAlign: TextAlign.center, - decoration: InputDecoration( - border: InputBorder.none, - ), - ), - ) - : ListView( - shrinkWrap: true, - padding: padding, - children: [ - LinkText( - text: status.statusMsg, - textAlign: TextAlign.center, - textStyle: TextStyle( - fontSize: 30, - color: Colors.white, - ), - linkStyle: TextStyle( - fontSize: 30, - color: Colors.white70, - decoration: TextDecoration.underline, - ), - onLinkTap: (url) => UrlLauncher(context, url).launchUrl(), - ), - ], - ), - ), - floatingActionButton: - !composeMode && status.userId == Matrix.of(context).client.userID - ? null - : FloatingActionButton.extended( - backgroundColor: Theme.of(context).primaryColor, - icon: Icon(composeMode ? Icons.edit : Icons.message_outlined), - label: Text(composeMode - ? L10n.of(context).setStatus - : L10n.of(context).sendAMessage), - onPressed: () => composeMode - ? _setStatusAction(context) - : _sendMessageAction(context), - ), - ); - } -} diff --git a/linux/main.cc b/linux/main.cc index 058e617..e7c5c54 100644 --- a/linux/main.cc +++ b/linux/main.cc @@ -1,10 +1,6 @@ #include "my_application.h" int main(int argc, char** argv) { - // Only X11 is currently supported. - // Wayland support is being developed: https://github.com/flutter/flutter/issues/57932. - gdk_set_allowed_backends("x11"); - g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } diff --git a/pubspec.lock b/pubspec.lock index c03fe54..639c459 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -168,7 +168,14 @@ packages: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "1.0.0" + dapackages: + dependency: "direct dev" + description: + name: dapackages + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" dart_style: dependency: transitive description: @@ -183,6 +190,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.3" + downloads_path_provider_28: + dependency: "direct main" + description: + name: downloads_path_provider_28 + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" encrypt: dependency: transitive description: @@ -226,7 +240,7 @@ packages: name: file_chooser url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.1.6" file_picker: dependency: transitive description: @@ -295,6 +309,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + flutter_highlight: + dependency: transitive + description: + name: flutter_highlight + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" flutter_keyboard_visibility: dependency: transitive description: @@ -315,7 +336,7 @@ packages: name: flutter_local_notifications url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "3.0.1" flutter_local_notifications_platform_interface: dependency: transitive description: @@ -334,7 +355,7 @@ packages: name: flutter_matrix_html url: "https://pub.dartlang.org" source: hosted - version: "0.1.9" + version: "0.1.10" flutter_olm: dependency: "direct main" description: @@ -394,6 +415,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + highlight: + dependency: transitive + description: + name: highlight + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" html: dependency: transitive description: @@ -526,7 +554,7 @@ packages: name: matrix_link_text url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "0.3.2" meta: dependency: transitive description: @@ -554,7 +582,7 @@ packages: name: moor url: "https://pub.dartlang.org" source: hosted - version: "3.3.1" + version: "3.4.0" native_imaging: dependency: "direct main" description: @@ -605,7 +633,7 @@ packages: name: open_file url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.3" package_config: dependency: transitive description: @@ -640,7 +668,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.21" + version: "1.6.22" path_provider_linux: dependency: transitive description: @@ -676,6 +704,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.10.0-nullsafety.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.1+1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" petitparser: dependency: transitive description: @@ -773,7 +815,7 @@ packages: name: share url: "https://pub.dartlang.org" source: hosted - version: "0.6.5+3" + version: "0.6.5+4" shelf: dependency: transitive description: @@ -834,7 +876,7 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "1.3.1+1" + version: "1.3.2" sqflite_common: dependency: transitive description: @@ -925,7 +967,7 @@ packages: name: timezone url: "https://pub.dartlang.org" source: hosted - version: "0.5.7" + version: "0.5.9" typed_data: dependency: transitive description: @@ -960,7 +1002,7 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.7.5" + version: "5.7.8" url_launcher_linux: dependency: transitive description: @@ -981,14 +1023,14 @@ packages: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.8" + version: "1.0.9" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.4+1" + version: "0.1.5" url_launcher_windows: dependency: transitive description: @@ -1044,7 +1086,7 @@ packages: name: webview_flutter url: "https://pub.dartlang.org" source: hosted - version: "0.3.24" + version: "1.0.5" win32: dependency: transitive description: @@ -1081,5 +1123,5 @@ packages: source: hosted version: "0.1.2" sdks: - dart: ">=2.10.0-110 <=2.11.0-161.0.dev" - flutter: ">=1.20.0 <2.0.0" + dart: ">=2.10.2 <2.11.0" + flutter: ">=1.22.2 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 06fe228..6e9abec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 + cupertino_icons: ^1.0.0 famedlysdk: git: @@ -30,41 +30,43 @@ dependencies: ref: yiffed # path: ../famedlysdk - localstorage: ^3.0.1+4 - file_picker_cross: ^4.2.2 - image_picker: ^0.6.7+11 - url_launcher: ^5.7.2 + localstorage: ^3.0.3+6 + file_picker_cross: 4.2.2 + image_picker: ^0.6.7+12 + url_launcher: ^5.7.8 cached_network_image: ^2.3.3 - firebase_messaging: ^7.0.2 - flutter_local_notifications: ^2.0.0+1 + firebase_messaging: ^7.0.3 + flutter_local_notifications: ^3.0.1 # desktop_notifications: ^0.0.0-dev.4 // Currently blocked by: https://github.com/canonical/desktop_notifications.dart/issues/5 - matrix_link_text: ^0.3.1 - path_provider: ^1.5.1 - webview_flutter: ^0.3.19+9 - share: ^0.6.3+5 - flutter_secure_storage: ^3.3.4 - http: ^0.12.0+4 - universal_html: ^1.1.12 - receive_sharing_intent: ^1.3.3 - flutter_slidable: ^0.5.4 + matrix_link_text: ^0.3.2 + path_provider: ^1.6.22 + downloads_path_provider_28: ^0.1.0 + permission_handler: ^5.0.1+1 + webview_flutter: ^1.0.5 + share: ^0.6.5+4 + flutter_secure_storage: ^3.3.5 + http: ^0.12.2 + universal_html: ^1.2.3 + receive_sharing_intent: ^1.4.1 + flutter_slidable: ^0.5.7 photo_view: ^0.10.2 - flutter_sound: ^2.1.1 - open_file: ^3.0.1 - mime_type: ^0.3.0 - bot_toast: ^3.0.0 - flutter_matrix_html: ^0.1.9 - moor: ^3.3.1 + flutter_sound: 2.1.1 + open_file: ^3.0.3 + mime_type: ^0.3.2 + bot_toast: ^3.0.4 + flutter_matrix_html: ^0.1.10 + moor: ^3.4.0 sqlite3_flutter_libs: ^0.2.0 - sqlite3: ^0.1.4 - random_string: ^2.0.1 - flutter_typeahead: ^1.8.1 + sqlite3: ^0.1.7 + random_string: ^2.1.0 + flutter_typeahead: ^1.8.8 flutter_olm: ^1.0.1 intl: ^0.16.1 - intl_translation: ^0.17.9 + intl_translation: ^0.17.10+1 circular_check_box: ^1.0.4 flutter_localizations: sdk: flutter - sqflite: ^1.1.7 # Still used to obtain the database location + sqflite: ^1.3.2 # Still used to obtain the database location native_imaging: git: url: https://gitlab.com/famedly/libraries/native_imaging.git @@ -78,7 +80,8 @@ dev_dependencies: sdk: flutter flutter_launcher_icons: "^0.7.4" - pedantic: ^1.9.0 + pedantic: ^1.9.2 + dapackages: ^1.3.0 flutter_icons: android: "launcher_icon" diff --git a/snap/gui/fluffychat.desktop b/snap/gui/fluffychat.desktop new file mode 100755 index 0000000..8861487 --- /dev/null +++ b/snap/gui/fluffychat.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=FluffyChat +GenericName=Matrix Client +Comment=Chat with your friends +Exec=fluffychat +Icon=${SNAP}/meta/gui/fluffychat.png +Terminal=false +Type=Application +Categories=Network;Chat;InstantMessaging; diff --git a/snap/gui/fluffychat.png b/snap/gui/fluffychat.png new file mode 100644 index 0000000..7a46e62 Binary files /dev/null and b/snap/gui/fluffychat.png differ diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 26ac664..3b5bd17 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: fluffychat base: core18 -version: script +version: git summary: Open. Nonprofit. Cute ♥ description: | FluffyChat - Chat with your friends @@ -23,28 +23,63 @@ description: | Microblog: https://metalhead.club/@krille grade: devel +confinement: strict icon: assets/logo.png -confinement: devmode parts: - olm: # FIXME + olm: + plugin: cmake source: https://gitlab.matrix.org/matrix-org/olm.git source-type: git source-tag: 3.2.1 - plugin: cmake build-packages: - - build-essential - override-build: | - cd /root/parts/olm/src - cmake . -Bbuild - cmake --build build + - g++ fluffychat: - after: [olm] - plugin: dump - source: ./build/linux/release/bundle + plugin: flutter + source: . + flutter-target: lib/main.dart stage-packages: - libsqlite3-dev - + - libatk-bridge2.0-0 + - libatk1.0-0 + - libatspi2.0-0 + - libcairo-gobject2 + - libcairo2 + - libdatrie1 + - libegl1 + - libepoxy0 + - libfontconfig1 + - libfreetype6 + - libgdk-pixbuf2.0-0 + - libglvnd0 + - libgraphite2-3 + - libgtk-3-0 + - libharfbuzz0b + - libpango-1.0-0 + - libpangocairo-1.0-0 + - libpangoft2-1.0-0 + - libpixman-1-0 + - libpng16-16 + - libthai0 + - libwayland-client0 + - libwayland-cursor0 + - libwayland-egl1 + - libx11-6 + - libxau6 + - libxcb-render0 + - libxcb-shm0 + - libxcb1 + - libxcomposite1 + - libxcursor1 + - libxdamage1 + - libxdmcp6 + - libxext6 + - libxfixes3 + - libxi6 + - libxinerama1 + - libxkbcommon0 + - libxrandr2 + - libxrender1 slots: dbus-svc: interface: dbus @@ -55,7 +90,7 @@ apps: fluffychat: command: fluffychat extensions: - - gnome-3-28 + - flutter-dev plugs: - network - home