diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index cb65ec32..a03fb5c0 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2462,5 +2462,6 @@ "placeholders": { "sender": {} } - } + }, + "transparent": "Transparent" } \ No newline at end of file diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 4f8d4d24..8c6e5614 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 40710f60..b351b0bc 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -457,7 +457,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -546,7 +546,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -595,7 +595,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/lib/pages/chat/chat_app_bar_title.dart b/lib/pages/chat/chat_app_bar_title.dart index 1cb84b48..a77c7c29 100644 --- a/lib/pages/chat/chat_app_bar_title.dart +++ b/lib/pages/chat/chat_app_bar_title.dart @@ -37,7 +37,6 @@ class ChatAppBarTitle extends StatelessWidget { MatrixLocals(L10n.of(context)!), ), size: 32, - presenceUserId: room.directChatMatrixID, ), ), const SizedBox(width: 12), diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 04cb70e4..b1e67f10 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/pages/chat/events/message.dart'; import 'package:fluffychat/pages/chat/seen_by_row.dart'; import 'package:fluffychat/pages/chat/typing_indicators.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; +import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -36,6 +37,9 @@ class ChatEventList extends StatelessWidget { thisEventsKeyMap[events[i].eventId] = i; } + final hasWallpaper = + controller.room.client.applicationAccountConfig.wallpaperUrl != null; + return SelectionArea( child: ListView.custom( padding: EdgeInsets.only( @@ -140,6 +144,8 @@ class ChatEventList extends StatelessWidget { controller.readMarkerEventId == event.eventId && controller.timeline?.allowNewEvent == false, nextEvent: i + 1 < events.length ? events[i + 1] : null, + avatarPresenceBackgroundColor: + hasWallpaper ? Colors.transparent : null, ), ); }, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 9b8894a9..65dfb475 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -16,9 +16,11 @@ import 'package:fluffychat/pages/chat/pinned_events.dart'; import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/pages/chat/tombstone_display.dart'; +import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/widgets/connection_status_header.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:fluffychat/widgets/unread_rooms_badge.dart'; import '../../utils/stream_extension.dart'; import 'chat_emoji_picker.dart'; @@ -136,6 +138,8 @@ class ChatView extends StatelessWidget { final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0; final scrollUpBannerEventId = controller.scrollUpBannerEventId; + final accountConfig = Matrix.of(context).client.applicationAccountConfig; + return PopScope( canPop: controller.selectedEvents.isEmpty && !controller.showEmojiPicker, onPopInvoked: (pop) async { @@ -198,6 +202,18 @@ class ChatView extends StatelessWidget { onDragExited: controller.onDragExited, child: Stack( children: [ + if (accountConfig.wallpaperUrl != null) + Opacity( + opacity: accountConfig.wallpaperOpacity ?? 1, + child: MxcImage( + uri: accountConfig.wallpaperUrl, + fit: BoxFit.cover, + isThumbnail: true, + width: FluffyThemes.columnWidth * 2, + height: MediaQuery.of(context).size.height, + placeholder: (_) => Container(), + ), + ), SafeArea( child: Column( children: [ @@ -300,8 +316,9 @@ class ChatView extends StatelessWidget { children: [ TextButton.icon( style: TextButton.styleFrom( - padding: - const EdgeInsets.all(16), + padding: const EdgeInsets.all( + 16, + ), foregroundColor: Theme.of(context) .colorScheme @@ -317,8 +334,9 @@ class ChatView extends StatelessWidget { ), TextButton.icon( style: TextButton.styleFrom( - padding: - const EdgeInsets.all(16), + padding: const EdgeInsets.all( + 16, + ), ), icon: const Icon( Icons.forum_outlined, diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 2c802bcf..7eadce3e 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -33,6 +33,7 @@ class Message extends StatelessWidget { final bool highlightMarker; final bool animateIn; final void Function()? resetAnimateIn; + final Color? avatarPresenceBackgroundColor; const Message( this.event, { @@ -49,6 +50,7 @@ class Message extends StatelessWidget { this.highlightMarker = false, this.animateIn = false, this.resetAnimateIn, + this.avatarPresenceBackgroundColor, super.key, }); @@ -177,6 +179,7 @@ class Message extends StatelessWidget { mxContent: user.avatarUrl, name: user.calcDisplayname(), presenceUserId: user.stateKey, + presenceBackgroundColor: avatarPresenceBackgroundColor, onTap: () => onAvatarTab(event), ); }, diff --git a/lib/pages/settings_style/settings_style.dart b/lib/pages/settings_style/settings_style.dart index 267c69d9..58f8e2c1 100644 --- a/lib/pages/settings_style/settings_style.dart +++ b/lib/pages/settings_style/settings_style.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; +import 'package:fluffychat/utils/account_config.dart'; +import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/theme_builder.dart'; import '../../widgets/matrix.dart'; import 'settings_style_view.dart'; @@ -19,6 +24,51 @@ class SettingsStyleController extends State { ThemeController.of(context).setPrimaryColor(color); } + void setWallpaper() async { + final client = Matrix.of(context).client; + final picked = await AppLock.of(context).pauseWhile( + FilePicker.platform.pickFiles( + type: FileType.image, + withData: true, + ), + ); + final pickedFile = picked?.files.firstOrNull; + if (pickedFile == null) return; + + await showFutureLoadingDialog( + context: context, + future: () async { + final url = await client.uploadContent( + pickedFile.bytes!, + filename: pickedFile.name, + ); + await client.updateApplicationAccountConfig( + ApplicationAccountConfig(wallpaperUrl: url), + ); + }, + ); + } + + void setChatWallpaperOpacity(double opacity) { + final client = Matrix.of(context).client; + showFutureLoadingDialog( + context: context, + future: () => client.updateApplicationAccountConfig( + ApplicationAccountConfig(wallpaperOpacity: opacity), + ), + ); + } + + void deleteChatWallpaper() => showFutureLoadingDialog( + context: context, + future: () => Matrix.of(context).client.setApplicationAccountConfig( + const ApplicationAccountConfig( + wallpaperUrl: null, + wallpaperOpacity: null, + ), + ), + ); + ThemeMode get currentTheme => ThemeController.of(context).themeMode; Color? get currentColor => ThemeController.of(context).primaryColor; diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index ac65d8cd..787aafa9 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -3,7 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/utils/account_config.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; import '../../config/app_config.dart'; import 'settings_style.dart'; @@ -15,6 +19,7 @@ class SettingsStyleView extends StatelessWidget { @override Widget build(BuildContext context) { const colorPickerSize = 32.0; + final client = Matrix.of(context).client; return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), @@ -166,27 +171,104 @@ class SettingsStyleView extends StatelessWidget { ), ), ), - Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Material( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - child: Text( - 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer, - fontSize: - AppConfig.messageFontSize * AppConfig.fontSizeFactor, - ), - ), - ), + StreamBuilder( + stream: client.onAccountData.stream.where( + (data) => + data.type == + ApplicationAccountConfigExtension.accountDataKey, ), + builder: (context, snapshot) { + final accountConfig = client.applicationAccountConfig; + final wallpaperOpacity = accountConfig.wallpaperOpacity ?? 1; + final wallpaperOpacityIsDefault = wallpaperOpacity == 1; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + alignment: Alignment.centerLeft, + decoration: const BoxDecoration(), + clipBehavior: Clip.hardEdge, + child: Stack( + children: [ + if (accountConfig.wallpaperUrl != null) + Opacity( + opacity: wallpaperOpacity, + child: MxcImage( + uri: accountConfig.wallpaperUrl, + fit: BoxFit.cover, + isThumbnail: true, + width: FluffyThemes.columnWidth * 2, + height: 156, + ), + ), + Padding( + padding: EdgeInsets.only( + left: 12 + 12 + Avatar.defaultSize, + right: 12, + top: accountConfig.wallpaperUrl == null ? 0 : 12, + bottom: 12, + ), + child: Material( + color: Theme.of(context) + .colorScheme + .primaryContainer, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Text( + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + fontSize: AppConfig.messageFontSize * + AppConfig.fontSizeFactor, + ), + ), + ), + ), + ), + ], + ), + ), + ListTile( + title: Text(L10n.of(context)!.wallpaper), + leading: const Icon(Icons.photo_outlined), + trailing: accountConfig.wallpaperUrl == null + ? null + : IconButton( + icon: const Icon(Icons.delete_outlined), + color: Theme.of(context).colorScheme.error, + onPressed: controller.deleteChatWallpaper, + ), + onTap: controller.setWallpaper, + ), + AnimatedSize( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: accountConfig.wallpaperUrl != null + ? SwitchListTile.adaptive( + title: Text(L10n.of(context)!.transparent), + secondary: const Icon(Icons.blur_linear_outlined), + value: !wallpaperOpacityIsDefault, + onChanged: (_) => + controller.setChatWallpaperOpacity( + wallpaperOpacityIsDefault ? 0.4 : 1, + ), + ) + : null, + ), + ], + ); + }, ), ListTile( title: Text(L10n.of(context)!.fontSize), diff --git a/lib/utils/account_config.dart b/lib/utils/account_config.dart new file mode 100644 index 00000000..ee80e246 --- /dev/null +++ b/lib/utils/account_config.dart @@ -0,0 +1,65 @@ +import 'package:matrix/matrix.dart'; + +extension ApplicationAccountConfigExtension on Client { + static const String accountDataKey = 'im.fluffychat.account_config'; + + ApplicationAccountConfig get applicationAccountConfig => + ApplicationAccountConfig.fromJson( + accountData[accountDataKey]?.content ?? {}, + ); + + Future setApplicationAccountConfig( + ApplicationAccountConfig config, + ) => + setAccountData( + userID!, + accountDataKey, + config.toJson(), + ); + + /// Only updates the specified values in ApplicationAccountConfig + Future updateApplicationAccountConfig( + ApplicationAccountConfig config, + ) { + final currentConfig = applicationAccountConfig; + return setAccountData( + userID!, + accountDataKey, + ApplicationAccountConfig( + wallpaperUrl: config.wallpaperUrl ?? currentConfig.wallpaperUrl, + wallpaperOpacity: + config.wallpaperOpacity ?? currentConfig.wallpaperOpacity, + ).toJson(), + ); + } +} + +class ApplicationAccountConfig { + final Uri? wallpaperUrl; + final double? wallpaperOpacity; + + const ApplicationAccountConfig({ + this.wallpaperUrl, + this.wallpaperOpacity, + }); + + static double _sanitizedOpacity(double? opacity) { + if (opacity == null) return 1; + if (opacity > 1 || opacity < 0) return 1; + return opacity; + } + + factory ApplicationAccountConfig.fromJson(Map json) => + ApplicationAccountConfig( + wallpaperUrl: json['wallpaper_url'] is String + ? Uri.tryParse(json['wallpaper_url']) + : null, + wallpaperOpacity: + _sanitizedOpacity(json.tryGet('wallpaper_opacity')), + ); + + Map toJson() => { + 'wallpaper_url': wallpaperUrl?.toString(), + 'wallpaper_opacity': wallpaperOpacity, + }; +} diff --git a/lib/widgets/avatar.dart b/lib/widgets/avatar.dart index a9660ce9..23133fad 100644 --- a/lib/widgets/avatar.dart +++ b/lib/widgets/avatar.dart @@ -92,8 +92,8 @@ class Avatar extends StatelessWidget { ? Colors.orange : Colors.grey; return Positioned( - bottom: -4, - right: -4, + bottom: -3, + right: -3, child: Container( width: 16, height: 16, @@ -104,11 +104,15 @@ class Avatar extends StatelessWidget { ), alignment: Alignment.center, child: Container( - width: 8, - height: 8, + width: 10, + height: 10, decoration: BoxDecoration( color: dotColor, borderRadius: BorderRadius.circular(16), + border: Border.all( + width: 1, + color: Theme.of(context).colorScheme.background, + ), ), ), ),