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", "settingsKeyboardShortcutSettings": "Settings",
"settingsKeyboardShortcutNewMessage": "New Message", "settingsKeyboardShortcutNewMessage": "New Message",
"settingsKeyboardShortcutCloseDialog": "Close Dialog", "settingsKeyboardShortcutCloseDialog": "Close Dialog",
"settingsMessageDisplayStyle": "Message Display Style",
"close": "Close", "close": "Close",
"drafts": "Drafts", "drafts": "Drafts",
"noDrafts": "No drafts yet", "noDrafts": "No drafts yet",
@@ -1021,5 +1022,6 @@
"currentEmbed": "Current Embed", "currentEmbed": "Current Embed",
"noEmbed": "No embed yet", "noEmbed": "No embed yet",
"save": "Save", "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 kAppWindowSize = 'app_window_size';
const kAppEnterToSend = 'app_enter_to_send'; const kAppEnterToSend = 'app_enter_to_send';
const kAppDefaultPoolId = 'app_default_pool_id'; const kAppDefaultPoolId = 'app_default_pool_id';
const kAppMessageDisplayStyle = 'app_message_display_style';
const kFeaturedPostsCollapsedId = const kFeaturedPostsCollapsedId =
'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post '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 int? appColorScheme, // The color stored via the int type
required Size? windowSize, // The window size for desktop platforms required Size? windowSize, // The window size for desktop platforms
required String? defaultPoolId, required String? defaultPoolId,
required String messageDisplayStyle,
}) = _AppSettings; }) = _AppSettings;
} }
@@ -87,6 +89,7 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
appColorScheme: prefs.getInt(kAppColorSchemeStoreKey), appColorScheme: prefs.getInt(kAppColorSchemeStoreKey),
windowSize: _getWindowSizeFromPrefs(prefs), windowSize: _getWindowSizeFromPrefs(prefs),
defaultPoolId: prefs.getString(kAppDefaultPoolId), defaultPoolId: prefs.getString(kAppDefaultPoolId),
messageDisplayStyle: prefs.getString(kAppMessageDisplayStyle) ?? 'bubble',
); );
} }
@@ -106,6 +109,7 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
} }
return null; return null;
} }
void setDefaultPoolId(String? value) { void setDefaultPoolId(String? value) {
final prefs = ref.read(sharedPreferencesProvider); final prefs = ref.read(sharedPreferencesProvider);
if (value != null) { if (value != null) {
@@ -122,7 +126,7 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
state = state.copyWith(autoTranslate: value); state = state.copyWith(autoTranslate: value);
} }
void setDataSavingMode(bool value){ void setDataSavingMode(bool value) {
final prefs = ref.read(sharedPreferencesProvider); final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppDataSavingMode, value); prefs.setBool(kAppDataSavingMode, value);
state = state.copyWith(dataSavingMode: value); state = state.copyWith(dataSavingMode: value);
@@ -186,6 +190,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
Size? getWindowSize() { Size? getWindowSize() {
return state.windowSize; return state.windowSize;
} }
void setMessageDisplayStyle(String value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setString(kAppMessageDisplayStyle, value);
state = state.copyWith(messageDisplayStyle: value);
}
} }
final updateInfoProvider = 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 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 Size? get windowSize;// The window size for desktop platforms
String? get defaultPoolId; String? get defaultPoolId; String get messageDisplayStyle;
/// Create a copy of AppSettings /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -27,16 +27,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
@override @override
bool operator ==(Object other) { 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 @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 @override
String toString() { 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; factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_self.copyWith(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable 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 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 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 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 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) { switch (_that) {
case _AppSettings() when $default != null: 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(); 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) { switch (_that) {
case _AppSettings(): 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` /// 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) { switch (_that) {
case _AppSettings() when $default != null: 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; return null;
} }
@@ -212,7 +213,7 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
class _AppSettings implements AppSettings { 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; @override final bool autoTranslate;
@@ -228,6 +229,7 @@ class _AppSettings implements AppSettings {
@override final Size? windowSize; @override final Size? windowSize;
// The window size for desktop platforms // The window size for desktop platforms
@override final String? defaultPoolId; @override final String? defaultPoolId;
@override final String messageDisplayStyle;
/// Create a copy of AppSettings /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -239,16 +241,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
@override @override
bool operator ==(Object other) { 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 @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 @override
String toString() { 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; factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_AppSettings(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable 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 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 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 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 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() => String _$appSettingsNotifierHash() =>
r'a623ad859b71f42d0527b7f8b75bd37a6fd5d5c7'; r'9f0979f18b107e61185391e7c39bd81ac4b8ca50';
/// See also [AppSettingsNotifier]. /// See also [AppSettingsNotifier].
@ProviderFor(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:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/color_extraction.dart'; import 'package:island/services/color_extraction.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
@@ -35,7 +36,8 @@ class SettingsScreen extends HookConsumerWidget {
final isDesktop = final isDesktop =
!kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux); !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
final isWide = isWideScreen(context); final isWide = isWideScreen(context);
final poolsAsync = ref.watch(poolsProvider); final pools = ref.watch(poolsProvider);
final user = ref.watch(userInfoProvider);
final docBasepath = useState<String?>(null); final docBasepath = useState<String?>(null);
useEffect(() { 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 // Color scheme settings
ListTile( ListTile(
minLeadingWidth: 48, minLeadingWidth: 48,
@@ -370,65 +414,67 @@ class SettingsScreen extends HookConsumerWidget {
), ),
), ),
poolsAsync.when( if (user.value != null)
data: (pools) { pools.when(
final validPools = pools.filterValid(); data: (data) {
final currentPoolId = resolveDefaultPoolId(ref, pools); final validPools = data.filterValid();
final currentPoolId = resolveDefaultPoolId(ref, data);
return ListTile( return ListTile(
isThreeLine: true, 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(
minLeadingWidth: 48, minLeadingWidth: 48,
title: Text('settingsDefaultPool').tr(), title: Text('settingsDefaultPool').tr(),
subtitle: Text('Error: $err'), contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Icons.error, color: Colors.red), 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 = [ final behaviorSettings = [

View File

@@ -18,6 +18,32 @@ class MessageContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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) { switch (item.type) {
case 'call.start': case 'call.start':
case 'call.ended': 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': case 'text':
default: default:
return Column( return Column(

View File

@@ -13,18 +13,21 @@ import 'package:island/database/message.dart';
import 'package:island/models/embed.dart'; import 'package:island/models/embed.dart';
import 'package:island/pods/messages_notifier.dart'; import 'package:island/pods/messages_notifier.dart';
import 'package:island/pods/translate.dart'; import 'package:island/pods/translate.dart';
import 'package:island/pods/config.dart';
import 'package:island/screens/chat/room.dart'; import 'package:island/screens/chat/room.dart';
import 'package:island/utils/mapping.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/app_scaffold.dart';
import 'package:island/widgets/chat/message_content.dart'; import 'package:island/widgets/chat/message_content.dart';
import 'package:island/widgets/chat/message_indicators.dart'; import 'package:island/widgets/chat/message_indicators.dart';
import 'package:island/widgets/chat/message_sender_info.dart'; import 'package:island/widgets/chat/message_sender_info.dart';
import 'package:island/widgets/content/alert.native.dart'; import 'package:island/widgets/content/alert.native.dart';
import 'package:island/widgets/content/cloud_file_collection.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:island/widgets/content/embed/link.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:styled_widget/styled_widget.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 { class MessageItemAction {
static const String edit = "edit"; static const String edit = "edit";
@@ -51,6 +54,189 @@ class MessageItem extends HookConsumerWidget {
required this.onJump, 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final textColor = final textColor =
@@ -108,261 +294,455 @@ class MessageItem extends HookConsumerWidget {
final remoteMessage = message.toRemoteMessage(); final remoteMessage = message.toRemoteMessage();
final sender = remoteMessage.sender; final sender = remoteMessage.sender;
final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS); return Material(
color:
final messageLanguage = hasBackground
remoteMessage.content != null ? Colors.transparent
? ref.watch(detectStringLanguageProvider(remoteMessage.content!)) : Theme.of(context).colorScheme.surface,
: null; child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
final currentLanguage = context.locale.toString(); child: Column(
final translatableLanguage = crossAxisAlignment: CrossAxisAlignment.start,
messageLanguage != null mainAxisSize: MainAxisSize.min,
? 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(
children: [ children: [
if (isCurrentUser) if (showAvatar) ...[
MenuAction( const Gap(8),
title: 'edit'.tr(), MessageSenderInfo(
image: MenuImage.icon(Symbols.edit), sender: sender,
callback: () { createdAt: message.createdAt,
onAction!.call(MessageItemAction.edit); textColor: textColor,
},
), ),
if (isCurrentUser) const Gap(4),
MenuAction( ],
title: 'delete'.tr(), const Gap(2),
image: MenuImage.icon(Symbols.delete), Row(
callback: () { spacing: 4,
onAction!.call(MessageItemAction.delete); mainAxisSize: MainAxisSize.min,
}, crossAxisAlignment: CrossAxisAlignment.end,
), children: [
if (isCurrentUser) MenuSeparator(), Flexible(
MenuAction( child: AnimatedContainer(
title: 'reply'.tr(), duration: const Duration(milliseconds: 200),
image: MenuImage.icon(Symbols.reply), decoration: BoxDecoration(
callback: () { color: flashColor,
onAction!.call(MessageItemAction.reply); borderRadius: BorderRadius.circular(16),
}, ),
), padding: const EdgeInsets.symmetric(
MenuAction( horizontal: 12,
title: 'forward'.tr(), vertical: 6,
image: MenuImage.icon(Symbols.forward), ),
callback: () { child: Column(
onAction!.call(MessageItemAction.forward); crossAxisAlignment: CrossAxisAlignment.start,
}, children: [
), if (remoteMessage.repliedMessageId != null)
if (translatableLanguage) MenuSeparator(), MessageQuoteWidget(
if (translatableLanguage) message: message,
MenuAction( textColor: textColor,
title: isReply: true,
translatedText.value == null ).padding(vertical: 4),
? 'translate'.tr() if (remoteMessage.forwardedMessageId != null)
: translating.value MessageQuoteWidget(
? 'translating'.tr() message: message,
: 'translated'.tr(), textColor: textColor,
image: MenuImage.icon(Symbols.translate), isReply: false,
callback: translate, ).padding(vertical: 4),
), if (MessageContent.hasContent(remoteMessage))
if (isMobile) MenuSeparator(), MessageContent(
if (isMobile) item: remoteMessage,
MenuAction( translatedText: translatedText,
title: 'copyMessage'.tr(), ),
image: MenuImage.icon(Symbols.copy_all), if (remoteMessage.attachments.isNotEmpty)
callback: () { LayoutBuilder(
Clipboard.setData( builder: (context, constraints) {
ClipboardData(text: remoteMessage.content ?? ''), return CloudFileList(
); files: remoteMessage.attachments,
}, maxWidth: constraints.maxWidth,
), padding: EdgeInsets.symmetric(vertical: 4),
], );
); },
}, ),
child: Material( if (remoteMessage.meta['embeds'] != null)
color: ...((remoteMessage.meta['embeds'] as List<dynamic>)
hasBackground .map((embed) => convertMapKeysToSnakeCase(embed))
? Colors.transparent .where((embed) => embed['type'] == 'link')
: Theme.of(context).colorScheme.surface, .map((embed) => SnScrappedLink.fromJson(embed))
child: Padding( .map(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), (link) => LayoutBuilder(
child: Column( builder: (context, constraints) {
crossAxisAlignment: CrossAxisAlignment.start, return EmbedLinkWidget(
mainAxisSize: MainAxisSize.min, link: link,
children: [ maxWidth: math.min(
if (showAvatar) ...[ constraints.maxWidth,
const Gap(8), 480,
MessageSenderInfo( ),
sender: sender, margin: const EdgeInsets.symmetric(
createdAt: message.createdAt, 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, 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: [ children: [
Flexible( Row(
child: AnimatedContainer( spacing: 8,
duration: const Duration(milliseconds: 200), children: [
decoration: BoxDecoration( AccountPfcGestureDetector(
color: flashColor, uname: sender.account.name,
borderRadius: BorderRadius.circular(16), child: ProfilePictureWidget(
file: sender.account.profile.picture,
radius: kAvatarRadius,
),
), ),
padding: const EdgeInsets.symmetric( MessageSenderInfo(
horizontal: 12, sender: sender,
vertical: 6, createdAt: message.createdAt,
textColor: textColor,
showAvatar: false,
isCompact: true,
), ),
child: Column( ],
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ Column(
if (remoteMessage.repliedMessageId != null) crossAxisAlignment: CrossAxisAlignment.start,
MessageQuoteWidget( children: [
message: message, if (remoteMessage.repliedMessageId != null)
textColor: textColor, MessageQuoteWidget(
isReply: true, message: message,
).padding(vertical: 4), textColor: textColor,
if (remoteMessage.forwardedMessageId != null) isReply: true,
MessageQuoteWidget( ).padding(vertical: 4),
message: message, if (remoteMessage.forwardedMessageId != null)
textColor: textColor, MessageQuoteWidget(
isReply: false, message: message,
).padding(vertical: 4), textColor: textColor,
if (MessageContent.hasContent(remoteMessage)) isReply: false,
MessageContent( ).padding(vertical: 4),
item: remoteMessage, if (MessageContent.hasContent(remoteMessage))
translatedText: translatedText.value, MessageContent(
), item: remoteMessage,
if (remoteMessage.attachments.isNotEmpty) translatedText: translatedText,
LayoutBuilder( ),
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) { builder: (context, constraints) {
return CloudFileList( return EmbedLinkWidget(
files: remoteMessage.attachments, link: link,
maxWidth: constraints.maxWidth, maxWidth: math.min(constraints.maxWidth, 480),
padding: EdgeInsets.symmetric(vertical: 4), margin: const EdgeInsets.symmetric(
vertical: 4,
),
); );
}, },
), ),
if (remoteMessage.meta['embeds'] != null) )
...((remoteMessage.meta['embeds'] as List<dynamic>) .toList()),
.map( if (progress != null && progress!.isNotEmpty)
(embed) => convertMapKeysToSnakeCase(embed), Column(
) crossAxisAlignment: CrossAxisAlignment.stretch,
.where((embed) => embed['type'] == 'link') spacing: 8,
.map((embed) => SnScrappedLink.fromJson(embed)) children: [
.map( if ((remoteMessage.content?.isNotEmpty ?? false))
(link) => LayoutBuilder( const Gap(0),
builder: (context, constraints) { for (var entry in progress!.entries)
return EmbedLinkWidget(
link: link,
maxWidth: math.min(
constraints.maxWidth,
480,
),
margin: const EdgeInsets.symmetric(
vertical: 4,
),
);
},
),
)
.toList()),
if (progress != null && progress!.isNotEmpty)
Column( Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [ children: [
if ((remoteMessage.content?.isNotEmpty ?? Text(
false)) 'fileUploadingProgress'.tr(
const Gap(0), args: [
for (var entry in progress!.entries) (entry.key + 1).toString(),
Column( entry.value.toStringAsFixed(1),
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), 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, sender: sender,
createdAt: message.createdAt, createdAt: message.createdAt,
textColor: Theme.of(context).colorScheme.onSurfaceVariant, textColor: Theme.of(context).colorScheme.onSurfaceVariant,
compact: true, showAvatar: false,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
MessageContent(item: remoteMessage), MessageContent(item: remoteMessage),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:gap/gap.dart';
import 'package:island/models/chat.dart'; import 'package:island/models/chat.dart';
import 'package:island/widgets/account/account_name.dart'; import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/account_pfc.dart'; import 'package:island/widgets/account/account_pfc.dart';
@@ -9,14 +10,16 @@ class MessageSenderInfo extends StatelessWidget {
final SnChatMember sender; final SnChatMember sender;
final DateTime createdAt; final DateTime createdAt;
final Color textColor; final Color textColor;
final bool compact; final bool showAvatar;
final bool isCompact;
const MessageSenderInfo({ const MessageSenderInfo({
super.key, super.key,
required this.sender, required this.sender,
required this.createdAt, required this.createdAt,
required this.textColor, required this.textColor,
this.compact = false, this.showAvatar = true,
this.isCompact = false,
}); });
@override @override
@@ -28,11 +31,13 @@ class MessageSenderInfo extends StatelessWidget {
? DateFormat('MM/dd HH:mm').format(createdAt.toLocal()) ? DateFormat('MM/dd HH:mm').format(createdAt.toLocal())
: DateFormat('HH:mm').format(createdAt.toLocal()); : DateFormat('HH:mm').format(createdAt.toLocal());
if (compact) { if (isCompact) {
return Row( return Row(
spacing: 8, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [ children: [
if (!compact) if (showAvatar)
AccountPfcGestureDetector( AccountPfcGestureDetector(
uname: sender.account.name, uname: sender.account.name,
child: ProfilePictureWidget( child: ProfilePictureWidget(
@@ -40,6 +45,34 @@ class MessageSenderInfo extends StatelessWidget {
radius: 14, 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( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -84,13 +117,14 @@ class MessageSenderInfo extends StatelessWidget {
spacing: 8, spacing: 8,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
AccountPfcGestureDetector( if (showAvatar)
uname: sender.account.name, AccountPfcGestureDetector(
child: ProfilePictureWidget( uname: sender.account.name,
fileId: sender.account.profile.picture?.id, child: ProfilePictureWidget(
radius: 16, fileId: sender.account.profile.picture?.id,
radius: 16,
),
), ),
),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 2, spacing: 2,