diff --git a/CHANGELOG.md b/CHANGELOG.md index 1124e38..5d7c031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,18 @@ # Version 0.20.0 - 2020-??-?? ### Features +- Added translations: Arabic - Add ability to enable / disable emotes globally - Add ability to manage emote packs with different state keys +- Add swipe to reply - Thanks @inexcode +- Initial support for compiling to desktop +- Initial snap metadata - Thanks @RAOF_47 ### Changes - Re-scale images in a separate isolate to prevent the UI from freezing +- URLs without https:// now linkify ### Fixes - Fix amoled / theme settings not always saving properly - Show device name in account information correctly +- Fix tapping on aliases / room pills not always working # Version 0.19.0 - 2020-09-21 ### Features diff --git a/lib/components/avatar.dart b/lib/components/avatar.dart index d5eaca2..fcc665e 100644 --- a/lib/components/avatar.dart +++ b/lib/components/avatar.dart @@ -3,7 +3,6 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../utils/platform_infos.dart'; import '../utils/string_color.dart'; import 'matrix.dart'; @@ -28,31 +27,49 @@ class Avatar extends StatelessWidget { Matrix.of(context).client, width: size * MediaQuery.of(context).devicePixelRatio, height: size * MediaQuery.of(context).devicePixelRatio, - method: ThumbnailMethod.scale, ); final src = thumbnail; var fallbackLetters = '@'; if ((name?.length ?? 0) >= 2) { - fallbackLetters = name.substring(0, 2); + fallbackLetters = String.fromCharCodes(name.runes, 0, 2); } else if ((name?.length ?? 0) == 1) { fallbackLetters = name; } + final textWidget = Center( + child: Text( + fallbackLetters, + style: TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + ); final noPic = mxContent == null || mxContent.toString().isEmpty; return InkWell( onTap: onTap, - child: CircleAvatar( - radius: size / 2, - backgroundImage: !noPic - ? PlatformInfos.isBetaDesktop - ? NetworkImage(src) - : CachedNetworkImageProvider(src) - : null, - backgroundColor: noPic - ? name?.lightColor ?? Theme.of(context).secondaryHeaderColor - : Theme.of(context).secondaryHeaderColor, - child: noPic - ? Text(fallbackLetters, style: TextStyle(color: Colors.white)) - : null, + child: ClipRRect( + borderRadius: BorderRadius.circular(size / 2), + child: Container( + width: size, + height: size, + color: noPic + ? name?.lightColor ?? Theme.of(context).secondaryHeaderColor + : Theme.of(context).secondaryHeaderColor, + child: noPic + ? textWidget + : CachedNetworkImage( + imageUrl: src, + fit: BoxFit.cover, + width: size, + height: size, + placeholder: (c, s) => Stack( + children: [ + Center(child: CircularProgressIndicator(strokeWidth: 2)), + textWidget, + ], + ), + ), + ), ), ); } diff --git a/lib/components/content_banner.dart b/lib/components/content_banner.dart index ed582e5..fc6f7de 100644 --- a/lib/components/content_banner.dart +++ b/lib/components/content_banner.dart @@ -49,17 +49,11 @@ class ContentBanner extends StatelessWidget { opacity: 0.75, child: !loading ? mxContent != null - ? PlatformInfos.isBetaDesktop - ? Image.network( - src, - height: 300, - fit: BoxFit.cover, - ) - : CachedNetworkImage( - imageUrl: src, - height: 300, - fit: BoxFit.cover, - ) + ? CachedNetworkImage( + imageUrl: src, + height: 300, + fit: BoxFit.cover, + ) : Icon(defaultIcon, size: 300) : Icon(defaultIcon, size: 300), ), diff --git a/lib/components/image_bubble.dart b/lib/components/image_bubble.dart index 232f044..6d09edb 100644 --- a/lib/components/image_bubble.dart +++ b/lib/components/image_bubble.dart @@ -128,17 +128,11 @@ class _ImageBubbleState extends State { width: 800, height: 800, method: ThumbnailMethod.scale); - renderWidget = PlatformInfos.isBetaDesktop - ? Image.network( - src, - fit: widget.fit, - ) - : CachedNetworkImage( - imageUrl: src, - placeholder: (context, url) => - generatePlaceholderWidget(), - fit: widget.fit, - ); + renderWidget = CachedNetworkImage( + imageUrl: src, + placeholder: (context, url) => generatePlaceholderWidget(), + fit: widget.fit, + ); } else { renderWidget = generatePlaceholderWidget(); } diff --git a/lib/components/input_bar.dart b/lib/components/input_bar.dart index 9b3a08d..5056218 100644 --- a/lib/components/input_bar.dart +++ b/lib/components/input_bar.dart @@ -150,17 +150,11 @@ class InputBar extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - PlatformInfos.isBetaDesktop - ? Image.network( - url, - width: size, - height: size, - ) - : CachedNetworkImage( - imageUrl: url, - width: size, - height: size, - ), + CachedNetworkImage( + imageUrl: url, + width: size, + height: size, + ), SizedBox(width: 6), Text(suggestion['name']), Expanded( diff --git a/lib/components/list_items/chat_list_item.dart b/lib/components/list_items/chat_list_item.dart index 1060b4e..ad21a67 100644 --- a/lib/components/list_items/chat_list_item.dart +++ b/lib/components/list_items/chat_list_item.dart @@ -1,4 +1,5 @@ import 'package:bot_toast/bot_toast.dart'; +import 'package:circular_check_box/circular_check_box.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -12,6 +13,7 @@ import '../avatar.dart'; import '../dialogs/send_file_dialog.dart'; import '../dialogs/simple_dialogs.dart'; import '../matrix.dart'; +import '../mouse_over_builder.dart'; import '../theme_switcher.dart'; class ChatListItem extends StatelessWidget { @@ -127,7 +129,20 @@ class ChatListItem extends StatelessWidget { color: chatListItemColor(context, activeChat, selected), child: ListTile( onLongPress: onLongPress, - leading: Avatar(room.avatar, room.displayname), + leading: MouseOverBuilder( + builder: (context, hover) => + onLongPress != null && (hover || selected) + ? Container( + width: Avatar.defaultSize, + height: Avatar.defaultSize, + alignment: Alignment.center, + child: CircularCheckBox( + value: selected, + onChanged: (_) => onLongPress(), + ), + ) + : Avatar(room.avatar, room.displayname), + ), title: Row( children: [ Expanded( diff --git a/lib/components/list_items/message.dart b/lib/components/list_items/message.dart index ba35e29..b459cc5 100644 --- a/lib/components/list_items/message.dart +++ b/lib/components/list_items/message.dart @@ -5,6 +5,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../../utils/date_time_extension.dart'; import '../../utils/event_extension.dart'; import '../../utils/string_color.dart'; +import '../adaptive_page_layout.dart'; import '../avatar.dart'; import '../dialogs/simple_dialogs.dart'; import '../matrix.dart'; @@ -86,6 +87,8 @@ class Message extends StatelessWidget { color: color, borderRadius: BorderRadius.circular(radius), ), + constraints: + BoxConstraints(maxWidth: AdaptivePageLayout.defaultMinWidth), child: Stack( children: [ Column( diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 03c0d41..241e072 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -8,6 +8,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:universal_html/prefer_universal/html.dart' as html; import 'package:url_launcher/url_launcher.dart'; +/*import 'package:fluffychat/views/chat.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:dbus/dbus.dart'; +import 'package:desktop_notifications/desktop_notifications.dart';*/ import '../utils/app_route.dart'; import '../utils/beautify_string_extension.dart'; @@ -179,9 +183,10 @@ class MatrixState extends State { bool webHasFocus = true; - void _showWebNotification(EventUpdate eventUpdate) async { - if (webHasFocus && activeRoomId == eventUpdate.roomID) return; - final room = client.getRoomById(eventUpdate.roomID); + void _showLocalNotification(EventUpdate eventUpdate) async { + final roomId = eventUpdate.roomID; + if (webHasFocus && activeRoomId == roomId) return; + final room = client.getRoomById(roomId); if (room.notificationCount == 0) return; final event = Event.fromJson(eventUpdate.content, room); final body = event.getLocalizedBody( @@ -189,20 +194,41 @@ class MatrixState extends State { withSenderNamePrefix: !room.isDirectChat || room.lastEvent.senderId == client.userID, ); - html.AudioElement() - ..src = 'assets/assets/sounds/notification.wav' - ..autoplay = true - ..load(); - html.Notification( - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), - body: body, - icon: event.sender.avatarUrl?.getThumbnail(client, - width: 64, height: 64, method: ThumbnailMethod.crop) ?? - room.avatar?.getThumbnail(client, - width: 64, height: 64, method: ThumbnailMethod.crop), - ); + final icon = event.sender.avatarUrl?.getThumbnail(client, + width: 64, height: 64, method: ThumbnailMethod.crop) ?? + room.avatar?.getThumbnail(client, + width: 64, height: 64, method: ThumbnailMethod.crop); + if (kIsWeb) { + html.AudioElement() + ..src = 'assets/assets/sounds/notification.wav' + ..autoplay = true + ..load(); + html.Notification( + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), + body: body, + icon: icon, + ); + } else if (Platform.isLinux) { + /*var sessionBus = DBusClient.session(); + var client = NotificationClient(sessionBus); + _linuxNotificationIds[roomId] = await client.notify( + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), + body: body, + replacesID: _linuxNotificationIds[roomId] ?? -1, + appName: AppConfig.applicationName, + actionCallback: (_) => Navigator.of(context).pushAndRemoveUntil( + AppRoute.defaultRoute( + context, + ChatView(roomId), + ), + (r) => r.isFirst), + ); + await sessionBus.close();*/ + } } + //final Map _linuxNotificationIds = {}; + @override void initState() { store = widget.store ?? Store(); @@ -299,7 +325,8 @@ class MatrixState extends State { if (kIsWeb) { onFocusSub = html.window.onFocus.listen((_) => webHasFocus = true); onBlurSub = html.window.onBlur.listen((_) => webHasFocus = false); - + } + if (kIsWeb || Platform.isLinux) { client.onSync.stream.first.then((s) { html.Notification.requestPermission(); onNotification ??= client.onEvent.stream @@ -308,7 +335,7 @@ class MatrixState extends State { [EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted] .contains(e.eventType) && e.content['sender'] != client.userID) - .listen(_showWebNotification); + .listen(_showLocalNotification); }); } super.initState(); diff --git a/lib/components/message_reactions.dart b/lib/components/message_reactions.dart index 9bd74c0..3822284 100644 --- a/lib/components/message_reactions.dart +++ b/lib/components/message_reactions.dart @@ -93,15 +93,10 @@ class _Reaction extends StatelessWidget { content = Row( mainAxisSize: MainAxisSize.min, children: [ - PlatformInfos.isBetaDesktop - ? Image.network( - src, - height: fontSize, - ) - : CachedNetworkImage( - imageUrl: src, - height: fontSize, - ), + CachedNetworkImage( + imageUrl: src, + height: fontSize, + ), Container(width: 4), Text(count.toString(), style: TextStyle( diff --git a/lib/components/mouse_over_builder.dart b/lib/components/mouse_over_builder.dart new file mode 100644 index 0000000..017eddc --- /dev/null +++ b/lib/components/mouse_over_builder.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class MouseOverBuilder extends StatefulWidget { + final Function(BuildContext, bool) builder; + + const MouseOverBuilder({Key key, this.builder}) : super(key: key); + @override + _MouseOverBuilderState createState() => _MouseOverBuilderState(); +} + +class _MouseOverBuilderState extends State { + bool _hover = false; + + void _toggleHover(bool hover) { + if (_hover != hover) { + setState(() => _hover = hover); + } + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => _toggleHover(true), + onExit: (_) => _toggleHover(false), + child: widget.builder != null ? widget.builder(context, _hover) : null, + ); + } +} diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 904fd44..4d736d8 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -683,14 +683,11 @@ class _ChatState extends State<_Chat> { body: Stack( children: [ if (Matrix.of(context).wallpaper != null) - Opacity( - opacity: 0.66, - child: Image.file( - Matrix.of(context).wallpaper, - height: double.infinity, - width: double.infinity, - fit: BoxFit.cover, - ), + Image.file( + Matrix.of(context).wallpaper, + height: double.infinity, + width: double.infinity, + fit: BoxFit.cover, ), Column( children: [ @@ -935,8 +932,7 @@ class _ChatState extends State<_Chat> { room.canSendDefaultMessages && room.membership == Membership.join ? Container( decoration: BoxDecoration( - color: - Theme.of(context).backgroundColor.withOpacity(0.8), + color: Theme.of(context).backgroundColor, ), child: Row( crossAxisAlignment: CrossAxisAlignment.end, diff --git a/lib/views/chat_list.dart b/lib/views/chat_list.dart index 8e0ad1c..fde309b 100644 --- a/lib/views/chat_list.dart +++ b/lib/views/chat_list.dart @@ -420,37 +420,34 @@ class _ChatListState extends State { ), ), ), - floatingActionButton: - (AdaptivePageLayout.columnMode(context) || - selectMode != SelectMode.normal) - ? 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: 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), + ), + ], + ), body: Column( children: [ ConnectionStatusHeader(), @@ -532,11 +529,7 @@ class _ChatListState extends State { (BuildContext context, int i) { if (i == 0) { final displayPresences = - Matrix.of(context) - .userStatuses - .isNotEmpty && - selectMode == - SelectMode.normal; + selectMode != SelectMode.share; final displayShareStatus = selectMode == SelectMode.share && diff --git a/lib/views/settings/settings_emotes.dart b/lib/views/settings/settings_emotes.dart index facb925..d995fcd 100644 --- a/lib/views/settings/settings_emotes.dart +++ b/lib/views/settings/settings_emotes.dart @@ -416,19 +416,12 @@ class _EmoteImage extends StatelessWidget { height: size * devicePixelRatio, method: ThumbnailMethod.scale, ); - return PlatformInfos.isBetaDesktop - ? Image.network( - url, - fit: BoxFit.contain, - width: size, - height: size, - ) - : CachedNetworkImage( - imageUrl: url, - fit: BoxFit.contain, - width: size, - height: size, - ); + return CachedNetworkImage( + imageUrl: url, + fit: BoxFit.contain, + width: size, + height: size, + ); } } diff --git a/linux/my_application.cc b/linux/my_application.cc index 2111adc..6620bc0 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -19,7 +19,7 @@ static void my_application_activate(GApplication* application) { gtk_header_bar_set_title(header_bar, "fluffychat"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - gtk_window_set_default_size(window, 1280, 720); + gtk_window_set_default_size(window, 802, 520); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); diff --git a/pubspec.lock b/pubspec.lock index 80f21f7..3d561a0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -77,7 +77,7 @@ packages: name: cached_network_image url: "https://pub.dartlang.org" source: hosted - version: "2.3.2+1" + version: "2.3.3" canonical_json: dependency: transitive description: @@ -99,6 +99,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0-nullsafety.1" + circular_check_box: + dependency: "direct main" + description: + name: circular_check_box + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" cli_util: dependency: transitive description: @@ -188,7 +195,7 @@ packages: description: path: "." ref: yiffed - resolved-ref: c1791edd4d9788d353c618cf1717a88d6c3b547b + resolved-ref: d9bc92d6543e51845ef384ee7650fcb3428d0718 url: "https://github.com/innereq/famedlysdk-fork.git" source: git version: "0.0.1" @@ -233,7 +240,7 @@ packages: name: firebase url: "https://pub.dartlang.org" source: hosted - version: "7.3.1" + version: "7.3.2" firebase_core: dependency: transitive description: @@ -280,7 +287,7 @@ packages: name: flutter_cache_manager url: "https://pub.dartlang.org" source: hosted - version: "1.4.2" + version: "2.0.0" flutter_keyboard_visibility: dependency: transitive description: @@ -301,7 +308,7 @@ packages: name: flutter_local_notifications url: "https://pub.dartlang.org" source: hosted - version: "2.0.0+1" + version: "2.0.1" flutter_local_notifications_platform_interface: dependency: transitive description: @@ -428,7 +435,7 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.6.7+11" + version: "0.6.7+12" image_picker_platform_interface: dependency: transitive description: @@ -477,7 +484,7 @@ packages: name: localstorage url: "https://pub.dartlang.org" source: hosted - version: "3.0.2+5" + version: "3.0.3+6" logging: dependency: transitive description: @@ -512,7 +519,7 @@ packages: name: matrix_link_text url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: @@ -626,7 +633,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.18" + version: "1.6.21" path_provider_linux: dependency: transitive description: @@ -759,7 +766,7 @@ packages: name: share url: "https://pub.dartlang.org" source: hosted - version: "0.6.5+2" + version: "0.6.5+3" shelf: dependency: transitive description: @@ -834,7 +841,7 @@ packages: name: sqlite3 url: "https://pub.dartlang.org" source: hosted - version: "0.1.6" + version: "0.1.7" sqlite3_flutter_libs: dependency: "direct main" description: @@ -946,7 +953,7 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.7.2" + version: "5.7.5" url_launcher_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 51725a1..0d054f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,10 +34,10 @@ dependencies: file_picker_cross: ^4.2.2 image_picker: ^0.6.7+11 url_launcher: ^5.7.2 - cached_network_image: ^2.3.2+1 + cached_network_image: ^2.3.3 firebase_messaging: ^7.0.2 flutter_local_notifications: ^2.0.0+1 - matrix_link_text: ^0.1.5 + matrix_link_text: ^0.2.0 path_provider: ^1.5.1 webview_flutter: ^0.3.19+9 share: ^0.6.3+5 @@ -60,6 +60,7 @@ dependencies: flutter_olm: ^1.0.1 intl: ^0.16.1 intl_translation: ^0.17.9 + circular_check_box: ^1.0.4 flutter_localizations: sdk: flutter sqflite: ^1.1.7 # Still used to obtain the database location