Compare commits

...

2 Commits

Author SHA1 Message Date
b0f3b6b5c3 Configure message style 2025-09-23 19:39:27 +08:00
cb2af379fa Provide three styles of message 2025-09-23 19:05:44 +08:00
9 changed files with 828 additions and 351 deletions

View File

@@ -473,6 +473,7 @@
"settingsKeyboardShortcutSettings": "Settings",
"settingsKeyboardShortcutNewMessage": "New Message",
"settingsKeyboardShortcutCloseDialog": "Close Dialog",
"settingsMessageDisplayStyle": "Message Display Style",
"close": "Close",
"drafts": "Drafts",
"noDrafts": "No drafts yet",
@@ -1021,5 +1022,6 @@
"currentEmbed": "Current Embed",
"noEmbed": "No embed yet",
"save": "Save",
"webView": "Web View"
"webView": "Web View",
"messageActions": "Message Actions"
}

View File

@@ -26,6 +26,7 @@ const kAppAprilFoolFeatures = 'app_april_fool_features';
const kAppWindowSize = 'app_window_size';
const kAppEnterToSend = 'app_enter_to_send';
const kAppDefaultPoolId = 'app_default_pool_id';
const kAppMessageDisplayStyle = 'app_message_display_style';
const kFeaturedPostsCollapsedId =
'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post
@@ -67,6 +68,7 @@ sealed class AppSettings with _$AppSettings {
required int? appColorScheme, // The color stored via the int type
required Size? windowSize, // The window size for desktop platforms
required String? defaultPoolId,
required String messageDisplayStyle,
}) = _AppSettings;
}
@@ -87,6 +89,7 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
appColorScheme: prefs.getInt(kAppColorSchemeStoreKey),
windowSize: _getWindowSizeFromPrefs(prefs),
defaultPoolId: prefs.getString(kAppDefaultPoolId),
messageDisplayStyle: prefs.getString(kAppMessageDisplayStyle) ?? 'bubble',
);
}
@@ -106,6 +109,7 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
}
return null;
}
void setDefaultPoolId(String? value) {
final prefs = ref.read(sharedPreferencesProvider);
if (value != null) {
@@ -122,7 +126,7 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
state = state.copyWith(autoTranslate: value);
}
void setDataSavingMode(bool value){
void setDataSavingMode(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppDataSavingMode, value);
state = state.copyWith(dataSavingMode: value);
@@ -186,6 +190,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
Size? getWindowSize() {
return state.windowSize;
}
void setMessageDisplayStyle(String value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setString(kAppMessageDisplayStyle, value);
state = state.copyWith(messageDisplayStyle: value);
}
}
final updateInfoProvider =

View File

@@ -16,7 +16,7 @@ mixin _$AppSettings {
bool get autoTranslate; bool get dataSavingMode; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; String? get customFonts; int? get appColorScheme;// The color stored via the int type
Size? get windowSize;// The window size for desktop platforms
String? get defaultPoolId;
String? get defaultPoolId; String get messageDisplayStyle;
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -27,16 +27,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId));
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle));
}
@override
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId);
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId,messageDisplayStyle);
@override
String toString() {
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId)';
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)';
}
@@ -47,7 +47,7 @@ abstract mixin class $AppSettingsCopyWith<$Res> {
factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
@useResult
$Res call({
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle
});
@@ -64,7 +64,7 @@ class _$AppSettingsCopyWithImpl<$Res>
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) {
return _then(_self.copyWith(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
@@ -77,7 +77,8 @@ as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts //
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
as Size?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
as String?,
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
as String,
));
}
@@ -159,10 +160,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AppSettings() when $default != null:
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId);case _:
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);case _:
return orElse();
}
@@ -180,10 +181,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle) $default,) {final _that = this;
switch (_that) {
case _AppSettings():
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId);}
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -197,10 +198,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle)? $default,) {final _that = this;
switch (_that) {
case _AppSettings() when $default != null:
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId);case _:
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);case _:
return null;
}
@@ -212,7 +213,7 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
class _AppSettings implements AppSettings {
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize, required this.defaultPoolId});
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize, required this.defaultPoolId, required this.messageDisplayStyle});
@override final bool autoTranslate;
@@ -228,6 +229,7 @@ class _AppSettings implements AppSettings {
@override final Size? windowSize;
// The window size for desktop platforms
@override final String? defaultPoolId;
@override final String messageDisplayStyle;
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@@ -239,16 +241,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle));
}
@override
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId);
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId,messageDisplayStyle);
@override
String toString() {
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId)';
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)';
}
@@ -259,7 +261,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith
factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
@override @useResult
$Res call({
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle
});
@@ -276,7 +278,7 @@ class __$AppSettingsCopyWithImpl<$Res>
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) {
return _then(_AppSettings(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
@@ -289,7 +291,8 @@ as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts //
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
as Size?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
as String?,
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
as String,
));
}

View File

@@ -7,7 +7,7 @@ part of 'config.dart';
// **************************************************************************
String _$appSettingsNotifierHash() =>
r'a623ad859b71f42d0527b7f8b75bd37a6fd5d5c7';
r'9f0979f18b107e61185391e7c39bd81ac4b8ca50';
/// See also [AppSettingsNotifier].
@ProviderFor(AppSettingsNotifier)

View File

@@ -12,6 +12,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/color_extraction.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
@@ -35,7 +36,8 @@ class SettingsScreen extends HookConsumerWidget {
final isDesktop =
!kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
final isWide = isWideScreen(context);
final poolsAsync = ref.watch(poolsProvider);
final pools = ref.watch(poolsProvider);
final user = ref.watch(userInfoProvider);
final docBasepath = useState<String?>(null);
useEffect(() {
@@ -129,6 +131,48 @@ class SettingsScreen extends HookConsumerWidget {
),
),
// Message display style settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsMessageDisplayStyle').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.chat),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
items: [
DropdownMenuItem<String>(
value: 'bubble',
child: Text('Bubble').fontSize(14),
),
DropdownMenuItem<String>(
value: 'discord',
child: Text('Discord').fontSize(14),
),
DropdownMenuItem<String>(
value: 'irc',
child: Text('IRC').fontSize(14),
),
],
value: settings.messageDisplayStyle,
onChanged: (String? value) {
if (value != null) {
ref
.read(appSettingsNotifierProvider.notifier)
.setMessageDisplayStyle(value);
showSnackBar('settingsApplied'.tr());
}
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
height: 40,
width: 140,
),
menuItemStyleData: const MenuItemStyleData(height: 40),
),
),
),
// Color scheme settings
ListTile(
minLeadingWidth: 48,
@@ -370,65 +414,67 @@ class SettingsScreen extends HookConsumerWidget {
),
),
poolsAsync.when(
data: (pools) {
final validPools = pools.filterValid();
final currentPoolId = resolveDefaultPoolId(ref, pools);
if (user.value != null)
pools.when(
data: (data) {
final validPools = data.filterValid();
final currentPoolId = resolveDefaultPoolId(ref, data);
return ListTile(
isThreeLine: true,
minLeadingWidth: 48,
title: Text('settingsDefaultPool').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.cloud),
subtitle: Text(
validPools
.firstWhereOrNull((p) => p.id == currentPoolId)
?.description ??
'settingsDefaultPoolHelper'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
items:
validPools.map((p) {
return DropdownMenuItem<String>(
value: p.id,
child: Text(p.name).fontSize(14),
);
}).toList(),
value: currentPoolId,
onChanged: (value) {
ref
.read(appSettingsNotifierProvider.notifier)
.setDefaultPoolId(value);
showSnackBar('settingsApplied'.tr());
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
height: 40,
width: 220,
),
menuItemStyleData: const MenuItemStyleData(height: 40),
),
),
);
},
loading:
() => const ListTile(
minLeadingWidth: 48,
title: Text('Loading pools...'),
leading: CircularProgressIndicator(),
),
error:
(err, st) => ListTile(
return ListTile(
isThreeLine: true,
minLeadingWidth: 48,
title: Text('settingsDefaultPool').tr(),
subtitle: Text('Error: $err'),
leading: const Icon(Icons.error, color: Colors.red),
),
),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.cloud),
subtitle: Text(
'settingsDefaultPoolHelper'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
items:
validPools.map((p) {
return DropdownMenuItem<String>(
value: p.id,
child: Text(
p.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
).fontSize(14),
);
}).toList(),
value: currentPoolId,
onChanged: (value) {
ref
.read(appSettingsNotifierProvider.notifier)
.setDefaultPoolId(value);
showSnackBar('settingsApplied'.tr());
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
height: 40,
width: 120,
),
menuItemStyleData: const MenuItemStyleData(height: 40),
),
),
);
},
loading:
() => const ListTile(
minLeadingWidth: 48,
title: Text('Loading pools...'),
leading: CircularProgressIndicator(),
),
error:
(err, st) => ListTile(
minLeadingWidth: 48,
title: Text('settingsDefaultPool').tr(),
subtitle: Text('Error: $err'),
leading: const Icon(Icons.error, color: Colors.red),
),
),
];
final behaviorSettings = [

View File

@@ -18,6 +18,32 @@ class MessageContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (item.type == 'messages.delete' || item.deletedAt != null) {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Symbols.delete,
size: 14,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
),
const Gap(4),
Text(
item.content ?? 'Deleted a message',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
fontStyle: FontStyle.italic,
),
),
],
);
}
switch (item.type) {
case 'call.start':
case 'call.ended':
@@ -71,30 +97,6 @@ class MessageContent extends StatelessWidget {
),
],
);
case 'messages.delete':
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Symbols.delete,
size: 14,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
),
const Gap(4),
Text(
item.content ?? 'Deleted a message',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
fontStyle: FontStyle.italic,
),
),
],
);
case 'text':
default:
return Column(

View File

@@ -13,18 +13,21 @@ import 'package:island/database/message.dart';
import 'package:island/models/embed.dart';
import 'package:island/pods/messages_notifier.dart';
import 'package:island/pods/translate.dart';
import 'package:island/pods/config.dart';
import 'package:island/screens/chat/room.dart';
import 'package:island/utils/mapping.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/message_content.dart';
import 'package:island/widgets/chat/message_indicators.dart';
import 'package:island/widgets/chat/message_sender_info.dart';
import 'package:island/widgets/content/alert.native.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/embed/link.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:super_context_menu/super_context_menu.dart';
import 'package:island/widgets/content/sheet.dart';
class MessageItemAction {
static const String edit = "edit";
@@ -51,6 +54,189 @@ class MessageItem extends HookConsumerWidget {
required this.onJump,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final remoteMessage = message.toRemoteMessage();
final settings = ref.watch(appSettingsNotifierProvider);
final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
final messageLanguage =
remoteMessage.content != null
? ref.watch(detectStringLanguageProvider(remoteMessage.content!))
: null;
final currentLanguage = context.locale.toString();
final translatableLanguage =
messageLanguage != null
? messageLanguage.substring(0, 2) != currentLanguage.substring(0, 2)
: false;
final translating = useState(false);
final translatedText = useState<String?>(null);
Future<void> translate() async {
if (translatedText.value != null) {
translatedText.value = null;
return;
}
if (translating.value) return;
if (remoteMessage.content == null) return;
translating.value = true;
try {
final text = await ref.watch(
translateStringProvider(
TranslateQuery(
text: remoteMessage.content!,
lang: currentLanguage.substring(0, 2),
),
).future,
);
translatedText.value = text;
} catch (err) {
showErrorAlert(err);
} finally {
translating.value = false;
}
}
void showActionMenu() {
if (onAction == null) return;
showModalBottomSheet(
context: context,
builder:
(context) => SheetScaffold(
titleText: 'messageActions'.tr(),
child: SingleChildScrollView(
child: Column(
children: [
if (isCurrentUser)
ListTile(
leading: Icon(Symbols.edit),
title: Text('edit'.tr()),
onTap: () {
onAction!.call(MessageItemAction.edit);
Navigator.pop(context);
},
),
if (isCurrentUser)
ListTile(
leading: Icon(Symbols.delete),
title: Text('delete'.tr()),
onTap: () {
onAction!.call(MessageItemAction.delete);
Navigator.pop(context);
},
),
if (isCurrentUser) Divider(),
ListTile(
leading: Icon(Symbols.reply),
title: Text('reply'.tr()),
onTap: () {
onAction!.call(MessageItemAction.reply);
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Symbols.forward),
title: Text('forward'.tr()),
onTap: () {
onAction!.call(MessageItemAction.forward);
Navigator.pop(context);
},
),
if (translatableLanguage) Divider(),
if (translatableLanguage)
ListTile(
leading: Icon(Symbols.translate),
title: Text(
translatedText.value == null
? 'translate'.tr()
: translating.value
? 'translating'.tr()
: 'translated'.tr(),
),
onTap: () {
translate();
Navigator.pop(context);
},
),
if (isMobile) Divider(),
if (isMobile)
ListTile(
leading: Icon(Symbols.copy_all),
title: Text('copyMessage'.tr()),
onTap: () {
Clipboard.setData(
ClipboardData(text: remoteMessage.content ?? ''),
);
Navigator.pop(context);
},
),
],
),
),
),
);
}
return GestureDetector(
onLongPress: showActionMenu,
onSecondaryTap: showActionMenu,
child: switch (settings.messageDisplayStyle) {
'irc' => MessageItemDisplayIRC(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
'discord' => MessageItemDisplayDiscord(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
_ => MessageItemDisplayBubble(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
},
);
}
}
class MessageItemDisplayBubble extends HookConsumerWidget {
final LocalChatMessage message;
final bool isCurrentUser;
final Map<int, double>? progress;
final bool showAvatar;
final Function(String messageId) onJump;
final String? translatedText;
final bool translating;
const MessageItemDisplayBubble({
super.key,
required this.message,
required this.isCurrentUser,
required this.progress,
required this.showAvatar,
required this.onJump,
required this.translatedText,
required this.translating,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final textColor =
@@ -108,261 +294,455 @@ class MessageItem extends HookConsumerWidget {
final remoteMessage = message.toRemoteMessage();
final sender = remoteMessage.sender;
final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
final messageLanguage =
remoteMessage.content != null
? ref.watch(detectStringLanguageProvider(remoteMessage.content!))
: null;
final currentLanguage = context.locale.toString();
final translatableLanguage =
messageLanguage != null
? messageLanguage.substring(0, 2) != currentLanguage.substring(0, 2)
: false;
final translating = useState(false);
final translatedText = useState<String?>(null);
Future<void> translate() async {
if (translatedText.value != null) {
translatedText.value = null;
return;
}
if (translating.value) return;
if (remoteMessage.content == null) return;
translating.value = true;
try {
final text = await ref.watch(
translateStringProvider(
TranslateQuery(
text: remoteMessage.content!,
lang: currentLanguage.substring(0, 2),
),
).future,
);
translatedText.value = text;
} catch (err) {
showErrorAlert(err);
} finally {
translating.value = false;
}
}
return ContextMenuWidget(
menuProvider: (_) {
if (onAction == null) return Menu(children: []);
return Menu(
return Material(
color:
hasBackground
? Colors.transparent
: Theme.of(context).colorScheme.surface,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (isCurrentUser)
MenuAction(
title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit),
callback: () {
onAction!.call(MessageItemAction.edit);
},
if (showAvatar) ...[
const Gap(8),
MessageSenderInfo(
sender: sender,
createdAt: message.createdAt,
textColor: textColor,
),
if (isCurrentUser)
MenuAction(
title: 'delete'.tr(),
image: MenuImage.icon(Symbols.delete),
callback: () {
onAction!.call(MessageItemAction.delete);
},
),
if (isCurrentUser) MenuSeparator(),
MenuAction(
title: 'reply'.tr(),
image: MenuImage.icon(Symbols.reply),
callback: () {
onAction!.call(MessageItemAction.reply);
},
),
MenuAction(
title: 'forward'.tr(),
image: MenuImage.icon(Symbols.forward),
callback: () {
onAction!.call(MessageItemAction.forward);
},
),
if (translatableLanguage) MenuSeparator(),
if (translatableLanguage)
MenuAction(
title:
translatedText.value == null
? 'translate'.tr()
: translating.value
? 'translating'.tr()
: 'translated'.tr(),
image: MenuImage.icon(Symbols.translate),
callback: translate,
),
if (isMobile) MenuSeparator(),
if (isMobile)
MenuAction(
title: 'copyMessage'.tr(),
image: MenuImage.icon(Symbols.copy_all),
callback: () {
Clipboard.setData(
ClipboardData(text: remoteMessage.content ?? ''),
);
},
),
],
);
},
child: Material(
color:
hasBackground
? Colors.transparent
: Theme.of(context).colorScheme.surface,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (showAvatar) ...[
const Gap(8),
MessageSenderInfo(
sender: sender,
createdAt: message.createdAt,
const Gap(4),
],
const Gap(2),
Row(
spacing: 4,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: flashColor,
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (remoteMessage.repliedMessageId != null)
MessageQuoteWidget(
message: message,
textColor: textColor,
isReply: true,
).padding(vertical: 4),
if (remoteMessage.forwardedMessageId != null)
MessageQuoteWidget(
message: message,
textColor: textColor,
isReply: false,
).padding(vertical: 4),
if (MessageContent.hasContent(remoteMessage))
MessageContent(
item: remoteMessage,
translatedText: translatedText,
),
if (remoteMessage.attachments.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
return CloudFileList(
files: remoteMessage.attachments,
maxWidth: constraints.maxWidth,
padding: EdgeInsets.symmetric(vertical: 4),
);
},
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.map((embed) => convertMapKeysToSnakeCase(embed))
.where((embed) => embed['type'] == 'link')
.map((embed) => SnScrappedLink.fromJson(embed))
.map(
(link) => LayoutBuilder(
builder: (context, constraints) {
return EmbedLinkWidget(
link: link,
maxWidth: math.min(
constraints.maxWidth,
480,
),
margin: const EdgeInsets.symmetric(
vertical: 4,
),
);
},
),
)
.toList()),
if (progress != null && progress!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 8,
children: [
if ((remoteMessage.content?.isNotEmpty ?? false))
const Gap(0),
for (var entry in progress!.entries)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'fileUploadingProgress'.tr(
args: [
(entry.key + 1).toString(),
entry.value.toStringAsFixed(1),
],
),
style: TextStyle(
fontSize: 12,
color: textColor.withOpacity(0.8),
),
),
const Gap(4),
LinearProgressIndicator(
value: entry.value / 100,
backgroundColor:
Theme.of(
context,
).colorScheme.surfaceVariant,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
),
const Gap(0),
],
),
],
),
),
),
MessageIndicators(
editedAt: remoteMessage.editedAt,
status: message.status,
isCurrentUser: isCurrentUser,
textColor: textColor,
),
const Gap(4),
],
const Gap(2),
Row(
spacing: 4,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
),
],
),
),
);
}
}
class MessageItemDisplayIRC extends HookConsumerWidget {
final LocalChatMessage message;
final bool isCurrentUser;
final Map<int, double>? progress;
final bool showAvatar;
final Function(String messageId) onJump;
final String? translatedText;
final bool translating;
const MessageItemDisplayIRC({
super.key,
required this.message,
required this.isCurrentUser,
required this.progress,
required this.showAvatar,
required this.onJump,
required this.translatedText,
required this.translating,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final remoteMessage = message.toRemoteMessage();
final sender = remoteMessage.sender;
final textColor = Theme.of(context).colorScheme.onSurfaceVariant;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
child: Row(
children: [
Text(
DateFormat('HH:mm').format(message.createdAt),
style: TextStyle(color: textColor.withOpacity(0.7), fontSize: 12),
),
const SizedBox(width: 8),
Text(
'<${sender.account.nick}>',
style: TextStyle(color: Colors.blue),
),
const SizedBox(width: 8),
Expanded(
child: Text(
translatedText ?? remoteMessage.content ?? '',
style: TextStyle(color: textColor),
),
),
],
),
);
}
}
class MessageItemDisplayDiscord extends HookConsumerWidget {
final LocalChatMessage message;
final bool isCurrentUser;
final Map<int, double>? progress;
final bool showAvatar;
final Function(String messageId) onJump;
final String? translatedText;
final bool translating;
const MessageItemDisplayDiscord({
super.key,
required this.message,
required this.isCurrentUser,
required this.progress,
required this.showAvatar,
required this.onJump,
required this.translatedText,
required this.translating,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final textColor = Theme.of(context).colorScheme.onSurfaceVariant;
final remoteMessage = message.toRemoteMessage();
final sender = remoteMessage.sender;
const kAvatarRadius = 12.0;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child:
showAvatar
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: flashColor,
borderRadius: BorderRadius.circular(16),
Row(
spacing: 8,
children: [
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
file: sender.account.profile.picture,
radius: kAvatarRadius,
),
),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
MessageSenderInfo(
sender: sender,
createdAt: message.createdAt,
textColor: textColor,
showAvatar: false,
isCompact: true,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (remoteMessage.repliedMessageId != null)
MessageQuoteWidget(
message: message,
textColor: textColor,
isReply: true,
).padding(vertical: 4),
if (remoteMessage.forwardedMessageId != null)
MessageQuoteWidget(
message: message,
textColor: textColor,
isReply: false,
).padding(vertical: 4),
if (MessageContent.hasContent(remoteMessage))
MessageContent(
item: remoteMessage,
translatedText: translatedText.value,
),
if (remoteMessage.attachments.isNotEmpty)
LayoutBuilder(
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (remoteMessage.repliedMessageId != null)
MessageQuoteWidget(
message: message,
textColor: textColor,
isReply: true,
).padding(vertical: 4),
if (remoteMessage.forwardedMessageId != null)
MessageQuoteWidget(
message: message,
textColor: textColor,
isReply: false,
).padding(vertical: 4),
if (MessageContent.hasContent(remoteMessage))
MessageContent(
item: remoteMessage,
translatedText: translatedText,
),
if (remoteMessage.attachments.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
return CloudFileList(
files: remoteMessage.attachments,
maxWidth: constraints.maxWidth,
padding: EdgeInsets.symmetric(vertical: 4),
);
},
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.map((embed) => convertMapKeysToSnakeCase(embed))
.where((embed) => embed['type'] == 'link')
.map((embed) => SnScrappedLink.fromJson(embed))
.map(
(link) => LayoutBuilder(
builder: (context, constraints) {
return EmbedLinkWidget(
link: link,
maxWidth: math.min(
constraints.maxWidth,
480,
),
margin: const EdgeInsets.symmetric(
vertical: 4,
),
);
},
),
)
.toList()),
if (progress != null && progress!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 8,
children: [
if ((remoteMessage.content?.isNotEmpty ?? false))
const SizedBox.shrink(),
for (var entry in progress!.entries)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'fileUploadingProgress'.tr(
args: [
(entry.key + 1).toString(),
entry.value.toStringAsFixed(1),
],
),
style: TextStyle(
fontSize: 12,
color: textColor.withOpacity(0.8),
),
),
const Gap(4),
LinearProgressIndicator(
value: entry.value / 100,
backgroundColor:
Theme.of(
context,
).colorScheme.surfaceVariant,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
),
const Gap(0),
],
),
],
).padding(left: kAvatarRadius * 2 + 8),
],
)
: Padding(
padding: EdgeInsets.only(left: kAvatarRadius * 2 + 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showAvatar)
MessageSenderInfo(
sender: sender,
createdAt: message.createdAt,
textColor: textColor,
showAvatar: false,
isCompact: true,
),
if (remoteMessage.repliedMessageId != null)
MessageQuoteWidget(
message: message,
textColor: textColor,
isReply: true,
).padding(vertical: 4),
if (remoteMessage.forwardedMessageId != null)
MessageQuoteWidget(
message: message,
textColor: textColor,
isReply: false,
).padding(vertical: 4),
if (MessageContent.hasContent(remoteMessage))
MessageContent(
item: remoteMessage,
translatedText: translatedText,
),
if (remoteMessage.attachments.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
return CloudFileList(
files: remoteMessage.attachments,
maxWidth: constraints.maxWidth,
padding: EdgeInsets.symmetric(vertical: 4),
);
},
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.map((embed) => convertMapKeysToSnakeCase(embed))
.where((embed) => embed['type'] == 'link')
.map((embed) => SnScrappedLink.fromJson(embed))
.map(
(link) => LayoutBuilder(
builder: (context, constraints) {
return CloudFileList(
files: remoteMessage.attachments,
maxWidth: constraints.maxWidth,
padding: EdgeInsets.symmetric(vertical: 4),
return EmbedLinkWidget(
link: link,
maxWidth: math.min(constraints.maxWidth, 480),
margin: const EdgeInsets.symmetric(
vertical: 4,
),
);
},
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.map(
(embed) => convertMapKeysToSnakeCase(embed),
)
.where((embed) => embed['type'] == 'link')
.map((embed) => SnScrappedLink.fromJson(embed))
.map(
(link) => LayoutBuilder(
builder: (context, constraints) {
return EmbedLinkWidget(
link: link,
maxWidth: math.min(
constraints.maxWidth,
480,
),
margin: const EdgeInsets.symmetric(
vertical: 4,
),
);
},
),
)
.toList()),
if (progress != null && progress!.isNotEmpty)
)
.toList()),
if (progress != null && progress!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 8,
children: [
if ((remoteMessage.content?.isNotEmpty ?? false))
const Gap(0),
for (var entry in progress!.entries)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if ((remoteMessage.content?.isNotEmpty ??
false))
const Gap(0),
for (var entry in progress!.entries)
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'fileUploadingProgress'.tr(
args: [
(entry.key + 1).toString(),
entry.value.toStringAsFixed(1),
],
),
style: TextStyle(
fontSize: 12,
color: textColor.withOpacity(0.8),
),
),
const Gap(4),
LinearProgressIndicator(
value: entry.value / 100,
backgroundColor:
Theme.of(
context,
).colorScheme.surfaceVariant,
valueColor:
AlwaysStoppedAnimation<Color>(
Theme.of(
context,
).colorScheme.primary,
),
),
Text(
'fileUploadingProgress'.tr(
args: [
(entry.key + 1).toString(),
entry.value.toStringAsFixed(1),
],
),
const Gap(0),
style: TextStyle(
fontSize: 12,
color: textColor.withOpacity(0.8),
),
),
const Gap(4),
LinearProgressIndicator(
value: entry.value / 100,
backgroundColor:
Theme.of(
context,
).colorScheme.surfaceVariant,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
),
const Gap(0),
],
),
),
),
MessageIndicators(
editedAt: remoteMessage.editedAt,
status: message.status,
isCurrentUser: isCurrentUser,
textColor: textColor,
),
],
],
),
),
],
),
),
),
);
}
}

View File

@@ -41,7 +41,7 @@ class MessageListTile extends StatelessWidget {
sender: sender,
createdAt: message.createdAt,
textColor: Theme.of(context).colorScheme.onSurfaceVariant,
compact: true,
showAvatar: false,
),
const SizedBox(height: 4),
MessageContent(item: remoteMessage),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:gap/gap.dart';
import 'package:island/models/chat.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/account_pfc.dart';
@@ -9,14 +10,16 @@ class MessageSenderInfo extends StatelessWidget {
final SnChatMember sender;
final DateTime createdAt;
final Color textColor;
final bool compact;
final bool showAvatar;
final bool isCompact;
const MessageSenderInfo({
super.key,
required this.sender,
required this.createdAt,
required this.textColor,
this.compact = false,
this.showAvatar = true,
this.isCompact = false,
});
@override
@@ -28,11 +31,13 @@ class MessageSenderInfo extends StatelessWidget {
? DateFormat('MM/dd HH:mm').format(createdAt.toLocal())
: DateFormat('HH:mm').format(createdAt.toLocal());
if (compact) {
if (isCompact) {
return Row(
spacing: 8,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
if (!compact)
if (showAvatar)
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
@@ -40,6 +45,34 @@ class MessageSenderInfo extends StatelessWidget {
radius: 14,
),
),
if (showAvatar) const Gap(4),
AccountName(
account: sender.account,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: textColor,
fontWeight: FontWeight.w500,
),
),
const Gap(6),
Text(
timestamp,
style: TextStyle(fontSize: 10, color: textColor.withOpacity(0.7)),
),
],
);
}
if (showAvatar) {
return Row(
spacing: 8,
children: [
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
radius: 14,
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -84,13 +117,14 @@ class MessageSenderInfo extends StatelessWidget {
spacing: 8,
mainAxisSize: MainAxisSize.min,
children: [
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
radius: 16,
if (showAvatar)
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
radius: 16,
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 2,