diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index ec8ad7e..d8618f2 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2,6 +2,10 @@ "@@locale": "en", "@@last_modified": "2025-06-05 12:38:37.885451", "noSendPermission": "You can't send messages here", + "noMessagesYet": "No messages yet", + "longPressToRecordVoiceMessage": "Long press to record voice message.", + "pause": "Pause", + "resume": "Resume", "@noSendPermission": {}, "alwaysUse24HourFormat": "false", "@alwaysUse24HourFormat": { diff --git a/assets/l10n/intl_ru.arb b/assets/l10n/intl_ru.arb index 5f3f75d..898fbc3 100644 --- a/assets/l10n/intl_ru.arb +++ b/assets/l10n/intl_ru.arb @@ -3,6 +3,10 @@ "@@last_modified": "2021-08-14 12:41:09.903021", "noSendPermission": "Вы не можете отправлять сообщения", "@noSendPermission": {}, + "noMessagesYet": "Нет сообщений", + "longPressToRecordVoiceMessage": "Зажмите, чтобы записать голосовое сообщение.", + "pause": "Пауза", + "resume": "Продолжить", "alwaysUse24HourFormat": "нет", "@alwaysUse24HourFormat": { "description": "Set to true to always display time of day in 24 hour format." diff --git a/lib/config/themes.dart b/lib/config/themes.dart index 8d4b199..4e1958a 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -40,7 +40,7 @@ abstract class FluffyThemes { BuildContext context, Brightness brightness, [ Color? seed, - bool? pureBlack, + bool? pureBlack, ]) { final extraDarkColors = (brightness == Brightness.dark && pureBlack == true) ? { @@ -118,6 +118,8 @@ abstract class FluffyThemes { isColumnMode ? colorScheme.surfaceContainer.withAlpha(128) : null, surfaceTintColor: isColumnMode ? colorScheme.surface : null, backgroundColor: isColumnMode ? colorScheme.surface : null, + actionsPadding: + isColumnMode ? const EdgeInsets.symmetric(horizontal: 16.0) : null, systemOverlayStyle: SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: brightness.reversed, @@ -140,6 +142,7 @@ abstract class FluffyThemes { ), snackBarTheme: isColumnMode ? const SnackBarThemeData( + showCloseIcon: true, behavior: SnackBarBehavior.floating, width: FluffyThemes.columnWidth * 1.5, ) diff --git a/lib/generated/l10n/l10n.dart b/lib/generated/l10n/l10n.dart index 3319e51..c160280 100644 --- a/lib/generated/l10n/l10n.dart +++ b/lib/generated/l10n/l10n.dart @@ -199,6 +199,30 @@ abstract class L10n { /// **'You can\'t send messages here'** String get noSendPermission; + /// No description provided for @noMessagesYet. + /// + /// In en, this message translates to: + /// **'No messages yet'** + String get noMessagesYet; + + /// No description provided for @longPressToRecordVoiceMessage. + /// + /// In en, this message translates to: + /// **'Long press to record voice message.'** + String get longPressToRecordVoiceMessage; + + /// No description provided for @pause. + /// + /// In en, this message translates to: + /// **'Pause'** + String get pause; + + /// No description provided for @resume. + /// + /// In en, this message translates to: + /// **'Resume'** + String get resume; + /// Set to true to always display time of day in 24 hour format. /// /// In en, this message translates to: diff --git a/lib/generated/l10n/l10n_ar.dart b/lib/generated/l10n/l10n_ar.dart index 11d38ac..dd35c62 100644 --- a/lib/generated/l10n/l10n_ar.dart +++ b/lib/generated/l10n/l10n_ar.dart @@ -11,6 +11,19 @@ class L10nAr extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_be.dart b/lib/generated/l10n/l10n_be.dart index dde88ff..16056f9 100644 --- a/lib/generated/l10n/l10n_be.dart +++ b/lib/generated/l10n/l10n_be.dart @@ -11,6 +11,19 @@ class L10nBe extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_bn.dart b/lib/generated/l10n/l10n_bn.dart index 50e94d0..4625641 100644 --- a/lib/generated/l10n/l10n_bn.dart +++ b/lib/generated/l10n/l10n_bn.dart @@ -11,6 +11,19 @@ class L10nBn extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_bo.dart b/lib/generated/l10n/l10n_bo.dart index 1c82c0d..476bb1b 100644 --- a/lib/generated/l10n/l10n_bo.dart +++ b/lib/generated/l10n/l10n_bo.dart @@ -11,6 +11,19 @@ class L10nBo extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_ca.dart b/lib/generated/l10n/l10n_ca.dart index f4935d7..8bc46ee 100644 --- a/lib/generated/l10n/l10n_ca.dart +++ b/lib/generated/l10n/l10n_ca.dart @@ -11,6 +11,19 @@ class L10nCa extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'true'; diff --git a/lib/generated/l10n/l10n_cs.dart b/lib/generated/l10n/l10n_cs.dart index 86aa967..a5de409 100644 --- a/lib/generated/l10n/l10n_cs.dart +++ b/lib/generated/l10n/l10n_cs.dart @@ -11,6 +11,19 @@ class L10nCs extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'Vypnuto'; diff --git a/lib/generated/l10n/l10n_de.dart b/lib/generated/l10n/l10n_de.dart index 6540fa8..f1452d8 100644 --- a/lib/generated/l10n/l10n_de.dart +++ b/lib/generated/l10n/l10n_de.dart @@ -11,6 +11,19 @@ class L10nDe extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'true'; diff --git a/lib/generated/l10n/l10n_el.dart b/lib/generated/l10n/l10n_el.dart index a3da084..639c673 100644 --- a/lib/generated/l10n/l10n_el.dart +++ b/lib/generated/l10n/l10n_el.dart @@ -11,6 +11,19 @@ class L10nEl extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_en.dart b/lib/generated/l10n/l10n_en.dart index 538137b..0a3b117 100644 --- a/lib/generated/l10n/l10n_en.dart +++ b/lib/generated/l10n/l10n_en.dart @@ -11,6 +11,19 @@ class L10nEn extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_eo.dart b/lib/generated/l10n/l10n_eo.dart index 00ea3b8..b990a2d 100644 --- a/lib/generated/l10n/l10n_eo.dart +++ b/lib/generated/l10n/l10n_eo.dart @@ -11,6 +11,19 @@ class L10nEo extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_es.dart b/lib/generated/l10n/l10n_es.dart index 2a1e41d..2d54dec 100644 --- a/lib/generated/l10n/l10n_es.dart +++ b/lib/generated/l10n/l10n_es.dart @@ -11,6 +11,19 @@ class L10nEs extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'falso'; diff --git a/lib/generated/l10n/l10n_et.dart b/lib/generated/l10n/l10n_et.dart index d9fe722..9bdd2a3 100644 --- a/lib/generated/l10n/l10n_et.dart +++ b/lib/generated/l10n/l10n_et.dart @@ -11,6 +11,19 @@ class L10nEt extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_eu.dart b/lib/generated/l10n/l10n_eu.dart index d90f5e3..2158531 100644 --- a/lib/generated/l10n/l10n_eu.dart +++ b/lib/generated/l10n/l10n_eu.dart @@ -11,6 +11,19 @@ class L10nEu extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_fa.dart b/lib/generated/l10n/l10n_fa.dart index 3b714b8..0e186e4 100644 --- a/lib/generated/l10n/l10n_fa.dart +++ b/lib/generated/l10n/l10n_fa.dart @@ -11,6 +11,19 @@ class L10nFa extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_fi.dart b/lib/generated/l10n/l10n_fi.dart index c9653f0..2b32fec 100644 --- a/lib/generated/l10n/l10n_fi.dart +++ b/lib/generated/l10n/l10n_fi.dart @@ -11,6 +11,19 @@ class L10nFi extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_fil.dart b/lib/generated/l10n/l10n_fil.dart index e1368a5..d1491cf 100644 --- a/lib/generated/l10n/l10n_fil.dart +++ b/lib/generated/l10n/l10n_fil.dart @@ -11,6 +11,19 @@ class L10nFil extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_fr.dart b/lib/generated/l10n/l10n_fr.dart index 47f596c..d3a6788 100644 --- a/lib/generated/l10n/l10n_fr.dart +++ b/lib/generated/l10n/l10n_fr.dart @@ -11,6 +11,19 @@ class L10nFr extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'true'; diff --git a/lib/generated/l10n/l10n_ga.dart b/lib/generated/l10n/l10n_ga.dart index 5320ce8..8a9040b 100644 --- a/lib/generated/l10n/l10n_ga.dart +++ b/lib/generated/l10n/l10n_ga.dart @@ -11,6 +11,19 @@ class L10nGa extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'bréagach'; diff --git a/lib/generated/l10n/l10n_gl.dart b/lib/generated/l10n/l10n_gl.dart index 471ee75..2077eb7 100644 --- a/lib/generated/l10n/l10n_gl.dart +++ b/lib/generated/l10n/l10n_gl.dart @@ -11,6 +11,19 @@ class L10nGl extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'falso'; diff --git a/lib/generated/l10n/l10n_he.dart b/lib/generated/l10n/l10n_he.dart index a6b4b62..1e31ce2 100644 --- a/lib/generated/l10n/l10n_he.dart +++ b/lib/generated/l10n/l10n_he.dart @@ -11,6 +11,19 @@ class L10nHe extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_hi.dart b/lib/generated/l10n/l10n_hi.dart index be12ea0..1a30903 100644 --- a/lib/generated/l10n/l10n_hi.dart +++ b/lib/generated/l10n/l10n_hi.dart @@ -11,6 +11,19 @@ class L10nHi extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_hr.dart b/lib/generated/l10n/l10n_hr.dart index 054e32f..d60b86f 100644 --- a/lib/generated/l10n/l10n_hr.dart +++ b/lib/generated/l10n/l10n_hr.dart @@ -11,6 +11,19 @@ class L10nHr extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'true'; diff --git a/lib/generated/l10n/l10n_hu.dart b/lib/generated/l10n/l10n_hu.dart index bc2505c..0c11ccf 100644 --- a/lib/generated/l10n/l10n_hu.dart +++ b/lib/generated/l10n/l10n_hu.dart @@ -11,6 +11,19 @@ class L10nHu extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'true'; diff --git a/lib/generated/l10n/l10n_ia.dart b/lib/generated/l10n/l10n_ia.dart index f0d54c8..8139635 100644 --- a/lib/generated/l10n/l10n_ia.dart +++ b/lib/generated/l10n/l10n_ia.dart @@ -11,6 +11,19 @@ class L10nIa extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_id.dart b/lib/generated/l10n/l10n_id.dart index d3ea89c..ae1d2b6 100644 --- a/lib/generated/l10n/l10n_id.dart +++ b/lib/generated/l10n/l10n_id.dart @@ -11,6 +11,19 @@ class L10nId extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'tidak'; diff --git a/lib/generated/l10n/l10n_ie.dart b/lib/generated/l10n/l10n_ie.dart index 6221d42..707362b 100644 --- a/lib/generated/l10n/l10n_ie.dart +++ b/lib/generated/l10n/l10n_ie.dart @@ -11,6 +11,19 @@ class L10nIe extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_it.dart b/lib/generated/l10n/l10n_it.dart index 187eb60..d27d01f 100644 --- a/lib/generated/l10n/l10n_it.dart +++ b/lib/generated/l10n/l10n_it.dart @@ -11,6 +11,19 @@ class L10nIt extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'disattivato'; diff --git a/lib/generated/l10n/l10n_ja.dart b/lib/generated/l10n/l10n_ja.dart index d1e10da..03b7f96 100644 --- a/lib/generated/l10n/l10n_ja.dart +++ b/lib/generated/l10n/l10n_ja.dart @@ -11,6 +11,19 @@ class L10nJa extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_ka.dart b/lib/generated/l10n/l10n_ka.dart index 9a7faf7..6897191 100644 --- a/lib/generated/l10n/l10n_ka.dart +++ b/lib/generated/l10n/l10n_ka.dart @@ -11,6 +11,19 @@ class L10nKa extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_ko.dart b/lib/generated/l10n/l10n_ko.dart index e0690fb..7799751 100644 --- a/lib/generated/l10n/l10n_ko.dart +++ b/lib/generated/l10n/l10n_ko.dart @@ -11,6 +11,19 @@ class L10nKo extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_lt.dart b/lib/generated/l10n/l10n_lt.dart index 068122a..bb4b57a 100644 --- a/lib/generated/l10n/l10n_lt.dart +++ b/lib/generated/l10n/l10n_lt.dart @@ -11,6 +11,19 @@ class L10nLt extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_lv.dart b/lib/generated/l10n/l10n_lv.dart index 4441882..9a0ca2c 100644 --- a/lib/generated/l10n/l10n_lv.dart +++ b/lib/generated/l10n/l10n_lv.dart @@ -11,6 +11,19 @@ class L10nLv extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'nē'; diff --git a/lib/generated/l10n/l10n_nb.dart b/lib/generated/l10n/l10n_nb.dart index df35e31..fb21189 100644 --- a/lib/generated/l10n/l10n_nb.dart +++ b/lib/generated/l10n/l10n_nb.dart @@ -11,6 +11,19 @@ class L10nNb extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_nl.dart b/lib/generated/l10n/l10n_nl.dart index ccccb49..1e2b7ef 100644 --- a/lib/generated/l10n/l10n_nl.dart +++ b/lib/generated/l10n/l10n_nl.dart @@ -11,6 +11,19 @@ class L10nNl extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'true'; diff --git a/lib/generated/l10n/l10n_pl.dart b/lib/generated/l10n/l10n_pl.dart index 0db8bdb..b8729bb 100644 --- a/lib/generated/l10n/l10n_pl.dart +++ b/lib/generated/l10n/l10n_pl.dart @@ -11,6 +11,19 @@ class L10nPl extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_pt.dart b/lib/generated/l10n/l10n_pt.dart index b4d3e77..d1aba0b 100644 --- a/lib/generated/l10n/l10n_pt.dart +++ b/lib/generated/l10n/l10n_pt.dart @@ -11,6 +11,19 @@ class L10nPt extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_ro.dart b/lib/generated/l10n/l10n_ro.dart index 5d4045e..b9d24f8 100644 --- a/lib/generated/l10n/l10n_ro.dart +++ b/lib/generated/l10n/l10n_ro.dart @@ -11,6 +11,19 @@ class L10nRo extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index e5154c3..8faade4 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -11,6 +11,19 @@ class L10nRu extends L10n { @override String get noSendPermission => 'Вы не можете отправлять сообщения'; + @override + String get noMessagesYet => 'Нет сообщений'; + + @override + String get longPressToRecordVoiceMessage => + 'Зажмите, чтобы записать голосовое сообщение.'; + + @override + String get pause => 'Пауза'; + + @override + String get resume => 'Продолжить'; + @override String get alwaysUse24HourFormat => 'нет'; diff --git a/lib/generated/l10n/l10n_sk.dart b/lib/generated/l10n/l10n_sk.dart index 2a4a4f4..2445303 100644 --- a/lib/generated/l10n/l10n_sk.dart +++ b/lib/generated/l10n/l10n_sk.dart @@ -11,6 +11,19 @@ class L10nSk extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_sl.dart b/lib/generated/l10n/l10n_sl.dart index d37a53a..e9a27d1 100644 --- a/lib/generated/l10n/l10n_sl.dart +++ b/lib/generated/l10n/l10n_sl.dart @@ -11,6 +11,19 @@ class L10nSl extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_sr.dart b/lib/generated/l10n/l10n_sr.dart index b1bc7c2..cb4c2a7 100644 --- a/lib/generated/l10n/l10n_sr.dart +++ b/lib/generated/l10n/l10n_sr.dart @@ -11,6 +11,19 @@ class L10nSr extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_sv.dart b/lib/generated/l10n/l10n_sv.dart index 6791f5e..895fb9f 100644 --- a/lib/generated/l10n/l10n_sv.dart +++ b/lib/generated/l10n/l10n_sv.dart @@ -11,6 +11,19 @@ class L10nSv extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_ta.dart b/lib/generated/l10n/l10n_ta.dart index 593d7ce..9f65c8c 100644 --- a/lib/generated/l10n/l10n_ta.dart +++ b/lib/generated/l10n/l10n_ta.dart @@ -11,6 +11,19 @@ class L10nTa extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'தவறு'; diff --git a/lib/generated/l10n/l10n_te.dart b/lib/generated/l10n/l10n_te.dart index fc230b8..c16161c 100644 --- a/lib/generated/l10n/l10n_te.dart +++ b/lib/generated/l10n/l10n_te.dart @@ -11,6 +11,19 @@ class L10nTe extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'తప్పుడు'; diff --git a/lib/generated/l10n/l10n_th.dart b/lib/generated/l10n/l10n_th.dart index 4cd734d..a4344e8 100644 --- a/lib/generated/l10n/l10n_th.dart +++ b/lib/generated/l10n/l10n_th.dart @@ -11,6 +11,19 @@ class L10nTh extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_tr.dart b/lib/generated/l10n/l10n_tr.dart index a406099..2afcd24 100644 --- a/lib/generated/l10n/l10n_tr.dart +++ b/lib/generated/l10n/l10n_tr.dart @@ -11,6 +11,19 @@ class L10nTr extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/generated/l10n/l10n_uk.dart b/lib/generated/l10n/l10n_uk.dart index aa676b0..9861508 100644 --- a/lib/generated/l10n/l10n_uk.dart +++ b/lib/generated/l10n/l10n_uk.dart @@ -11,6 +11,19 @@ class L10nUk extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'ні'; diff --git a/lib/generated/l10n/l10n_vi.dart b/lib/generated/l10n/l10n_vi.dart index f32bd98..c3482bb 100644 --- a/lib/generated/l10n/l10n_vi.dart +++ b/lib/generated/l10n/l10n_vi.dart @@ -11,6 +11,19 @@ class L10nVi extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'Không'; diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart index 62c8c90..a208315 100644 --- a/lib/generated/l10n/l10n_zh.dart +++ b/lib/generated/l10n/l10n_zh.dart @@ -11,6 +11,19 @@ class L10nZh extends L10n { @override String get noSendPermission => 'You can\'t send messages here'; + @override + String get noMessagesYet => 'No messages yet'; + + @override + String get longPressToRecordVoiceMessage => + 'Long press to record voice message.'; + + @override + String get pause => 'Pause'; + + @override + String get resume => 'Resume'; + @override String get alwaysUse24HourFormat => 'false'; diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 8e95006..3272989 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:extera_next/pages/chat/recovered_event_dialog.dart'; import 'package:extera_next/pages/chat/translated_event_dialog.dart'; -import 'package:extera_next/utils/file_description.dart'; import 'package:extera_next/utils/matrix_sdk_extensions/synapse_admin_extension.dart'; import 'package:extera_next/utils/translator.dart'; import 'package:flutter/foundation.dart'; @@ -18,7 +17,6 @@ import 'package:extera_next/generated/l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; -import 'package:record/record.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:universal_html/html.dart' as html; @@ -28,7 +26,6 @@ import 'package:extera_next/config/setting_keys.dart'; import 'package:extera_next/config/themes.dart'; import 'package:extera_next/pages/chat/chat_view.dart'; import 'package:extera_next/pages/chat/event_info_dialog.dart'; -import 'package:extera_next/pages/chat/recording_dialog.dart'; import 'package:extera_next/pages/chat_details/chat_details.dart'; import 'package:extera_next/utils/error_reporter.dart'; import 'package:extera_next/utils/file_selector.dart'; @@ -619,45 +616,39 @@ class ChatController extends State ); } - void voiceMessageAction() async { + Future onVoiceMessageSend( + String path, + int duration, + List waveform, + String? fileName, + ) async { final scaffoldMessenger = ScaffoldMessenger.of(context); - if (PlatformInfos.isAndroid) { - final info = await DeviceInfoPlugin().androidInfo; - if (info.version.sdkInt < 19) { - showOkAlertDialog( - context: context, - title: L10n.of(context).unsupportedAndroidVersion, - message: L10n.of(context).unsupportedAndroidVersionLong, - okLabel: L10n.of(context).close, - ); - return; - } - } + final audioFile = XFile(path); - if (await AudioRecorder().hasPermission() == false) return; - final result = await showDialog( + final bytesResult = await showFutureLoadingDialog( context: context, - barrierDismissible: false, - builder: (c) => const RecordingDialog(), + future: audioFile.readAsBytes, ); - if (result == null) return; - final audioFile = XFile(result.path); + final bytes = bytesResult.result; + if (bytes == null) return; + final file = MatrixAudioFile( - bytes: await audioFile.readAsBytes(), - name: result.fileName ?? audioFile.path, + bytes: bytes, + name: fileName ?? audioFile.path, ); + await room.sendFileEvent( file, inReplyTo: replyEvent, extraContent: { 'info': { ...file.info, - 'duration': result.duration, + 'duration': duration, }, 'org.matrix.msc3245.voice': {}, 'org.matrix.msc1767.audio': { - 'duration': result.duration, - 'waveform': result.waveform, + 'duration': duration, + 'waveform': waveform, }, }, ).catchError((e) { @@ -737,8 +728,11 @@ class ChatController extends State return; } final event = selectedEvents.single; - await mx.client.reportEvent(roomId, event.eventId, - reason: "Extera (Next) Redacted Event Recover"); + await mx.client.reportEvent( + roomId, + event.eventId, + reason: "Extera (Next) Redacted Event Recover", + ); final reports = await mx.client.getEventReports(); final report = reports.firstWhere( @@ -753,11 +747,14 @@ class ChatController extends State } Navigator.of(context).push(new MaterialPageRoute( - builder: (BuildContext ctx) { - return RecoveredEventDialog( - event: recoveredEvent!, timeline: timeline!); - }, - fullscreenDialog: true)); + builder: (BuildContext ctx) { + return RecoveredEventDialog( + event: recoveredEvent, + timeline: timeline!, + ); + }, + fullscreenDialog: true, + )); } void translateEventAction() async { @@ -769,12 +766,6 @@ class ChatController extends State } final event = selectedEvents.single; var text = event.isRichMessage ? event.formattedText : event.text; - if (text == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context).errorTranslatingMessage)), - ); - return; - } var content = {...event.content}; try { text = await Translator.translate( diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 6728c73..fbd4fa9 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:animations/animations.dart'; -import 'package:extera_next/generated/l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:extera_next/config/app_config.dart'; -import 'package:extera_next/utils/other_party_can_receive.dart'; +import 'package:extera_next/generated/l10n/l10n.dart'; +import 'package:extera_next/pages/chat/recording_input_row.dart'; +import 'package:extera_next/pages/chat/recording_view_model.dart'; import 'package:extera_next/utils/platform_infos.dart'; import 'package:extera_next/widgets/avatar.dart'; import 'package:extera_next/widgets/matrix.dart'; @@ -21,325 +22,331 @@ class ChatInputRow extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - if (controller.showEmojiPicker && - controller.emojiPickerType == EmojiPickerType.reaction) { - return const SizedBox.shrink(); - } + const height = 48.0; - if (!controller.room.canSendDefaultMessages) { - return Center( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Text( - L10n.of(context).noSendPermission, - style: theme.textTheme.bodySmall, - textAlign: TextAlign.center, - ), - ), - ); - } + final selectedTextButtonStyle = TextButton.styleFrom( + foregroundColor: theme.colorScheme.onTertiaryContainer, + ); - // if (!controller.room.otherPartyCanReceiveMessages) { - // return Center( - // child: Padding( - // padding: const EdgeInsets.all(12.0), - // child: Text( - // L10n.of(context).otherPartyNotLoggedIn, - // style: theme.textTheme.bodySmall, - // textAlign: TextAlign.center, - // ), - // ), - // ); - // } - - - - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: controller.selectMode - ? [ - if (controller.selectedEvents - .every((event) => event.status == EventStatus.error)) - SizedBox( - height: height, - child: TextButton( - style: TextButton.styleFrom( - foregroundColor: theme.colorScheme.error, + return RecordingViewModel( + builder: (context, recordingViewModel) { + if (recordingViewModel.isRecording) { + return RecordingInputRow( + state: recordingViewModel, + onSend: controller.onVoiceMessageSend, + ); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: controller.selectMode + ? [ + if (controller.selectedEvents + .every((event) => event.status == EventStatus.error)) + SizedBox( + height: height, + child: TextButton( + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.error, + ), + onPressed: controller.deleteErrorEventsAction, + child: Row( + children: [ + const Icon(Icons.delete_forever_outlined), + Text(L10n.of(context).delete), + ], + ), + ), + ) + else + SizedBox( + height: height, + child: TextButton( + style: selectedTextButtonStyle, + onPressed: controller.forwardEventsAction, + child: Row( + children: [ + const Icon(Icons.keyboard_arrow_left_outlined), + Text(L10n.of(context).forward), + ], + ), + ), ), - onPressed: controller.deleteErrorEventsAction, - child: Row( - children: [ - const Icon(Icons.delete), - Text(L10n.of(context).delete), + controller.selectedEvents.length == 1 + ? controller.selectedEvents.first + .getDisplayEvent(controller.timeline!) + .status + .isSent + ? SizedBox( + height: height, + child: TextButton( + style: selectedTextButtonStyle, + onPressed: controller.replyAction, + child: Row( + children: [ + Text(L10n.of(context).reply), + const Icon(Icons.keyboard_arrow_right), + ], + ), + ), + ) + : SizedBox( + height: height, + child: TextButton( + style: selectedTextButtonStyle, + onPressed: controller.sendAgainAction, + child: Row( + children: [ + Text(L10n.of(context).tryToSendAgain), + const SizedBox(width: 4), + const Icon(Icons.send_outlined, size: 16), + ], + ), + ), + ) + : const SizedBox.shrink(), + ] + : [ + const SizedBox(width: 4), + AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + width: + controller.sendController.text.isNotEmpty ? 0 : height, + height: height, + alignment: Alignment.center, + decoration: const BoxDecoration(), + clipBehavior: Clip.hardEdge, + child: PopupMenuButton( + useRootNavigator: true, + icon: const Icon(Icons.add_circle_outline), + iconColor: theme.colorScheme.onPrimaryContainer, + onSelected: controller.onAddPopupMenuButtonSelected, + itemBuilder: (BuildContext context) => + >[ + if (PlatformInfos.isMobile) + PopupMenuItem( + value: 'location', + child: ListTile( + leading: CircleAvatar( + backgroundColor: + theme.colorScheme.onPrimaryContainer, + foregroundColor: + theme.colorScheme.primaryContainer, + child: const Icon(Icons.gps_fixed_outlined), + ), + title: Text(L10n.of(context).shareLocation), + contentPadding: const EdgeInsets.all(0), + ), + ), + // PopupMenuItem( + // value: 'image', + // child: ListTile( + // leading: CircleAvatar( + // backgroundColor: + // theme.colorScheme.onPrimaryContainer, + // foregroundColor: + // theme.colorScheme.primaryContainer, + // child: const Icon(Icons.photo_outlined), + // ), + // title: Text(L10n.of(context).sendImage), + // contentPadding: const EdgeInsets.all(0), + // ), + // ), + // PopupMenuItem( + // value: 'video', + // child: ListTile( + // leading: CircleAvatar( + // backgroundColor: + // theme.colorScheme.onPrimaryContainer, + // foregroundColor: + // theme.colorScheme.primaryContainer, + // child: + // const Icon(Icons.video_camera_back_outlined), + // ), + // title: Text(L10n.of(context).sendVideo), + // contentPadding: const EdgeInsets.all(0), + // ), + // ), + PopupMenuItem( + value: 'file', + child: ListTile( + leading: CircleAvatar( + backgroundColor: + theme.colorScheme.onPrimaryContainer, + foregroundColor: + theme.colorScheme.primaryContainer, + child: const Icon(Icons.attachment_outlined), + ), + title: Text(L10n.of(context).sendFile), + contentPadding: const EdgeInsets.all(0), + ), + ), ], ), ), - ) - else - SizedBox( - height: height, - child: TextButton( - onPressed: controller.forwardEventsAction, - child: Row( - children: [ - const Icon(Icons.keyboard_arrow_left_outlined), - Text(L10n.of(context).forward), - ], - ), - ), - ), - controller.selectedEvents.length == 1 - ? controller.selectedEvents.first - .getDisplayEvent(controller.timeline!) - .status - .isSent - ? SizedBox( - height: height, - child: TextButton( - onPressed: controller.replyAction, - child: Row( - children: [ - Text(L10n.of(context).reply), - const Icon(Icons.keyboard_arrow_right), - ], + if (PlatformInfos.isMobile) + AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + width: controller.sendController.text.isNotEmpty + ? 0 + : height, + height: height, + alignment: Alignment.center, + decoration: const BoxDecoration(), + clipBehavior: Clip.hardEdge, + child: PopupMenuButton( + useRootNavigator: true, + icon: const Icon(Icons.camera_alt_outlined), + onSelected: controller.onAddPopupMenuButtonSelected, + iconColor: theme.colorScheme.onPrimaryContainer, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'camera-video', + child: ListTile( + leading: CircleAvatar( + backgroundColor: + theme.colorScheme.onPrimaryContainer, + foregroundColor: + theme.colorScheme.primaryContainer, + child: const Icon(Icons.videocam_outlined), + ), + title: Text(L10n.of(context).recordAVideo), + contentPadding: const EdgeInsets.all(0), ), ), - ) - : SizedBox( - height: height, - child: TextButton( - onPressed: controller.sendAgainAction, - child: Row( - children: [ - Text(L10n.of(context).tryToSendAgain), - const SizedBox(width: 4), - const Icon(Icons.send_outlined, size: 16), - ], + PopupMenuItem( + value: 'camera', + child: ListTile( + leading: CircleAvatar( + backgroundColor: + theme.colorScheme.onPrimaryContainer, + foregroundColor: + theme.colorScheme.primaryContainer, + child: const Icon(Icons.camera_alt_outlined), + ), + title: Text(L10n.of(context).takeAPhoto), + contentPadding: const EdgeInsets.all(0), ), ), - ) - : const SizedBox.shrink(), - ] - : [ - const SizedBox(width: 4), - AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - width: controller.sendController.text.isNotEmpty ? 0 : height, - height: height, - alignment: Alignment.center, - decoration: const BoxDecoration(), - clipBehavior: Clip.hardEdge, - child: PopupMenuButton( - icon: const Icon(Icons.add_circle_outline), - iconColor: theme.colorScheme.onPrimaryContainer, - onSelected: controller.onAddPopupMenuButtonSelected, - itemBuilder: (BuildContext context) => - >[ - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'location', - child: ListTile( - leading: CircleAvatar( - backgroundColor: - theme.colorScheme.onPrimaryContainer, - foregroundColor: theme.colorScheme.primaryContainer, - child: const Icon(Icons.gps_fixed_outlined), - ), - title: Text(L10n.of(context).shareLocation), - contentPadding: const EdgeInsets.all(0), - ), - ), - PopupMenuItem( - value: 'image', - child: ListTile( - leading: CircleAvatar( - backgroundColor: theme.colorScheme.onPrimaryContainer, - foregroundColor: theme.colorScheme.primaryContainer, - child: const Icon(Icons.photo_outlined), - ), - title: Text(L10n.of(context).sendImage), - contentPadding: const EdgeInsets.all(0), + ], ), ), - PopupMenuItem( - value: 'video', - child: ListTile( - leading: CircleAvatar( - backgroundColor: theme.colorScheme.onPrimaryContainer, - foregroundColor: theme.colorScheme.primaryContainer, - child: const Icon(Icons.video_camera_back_outlined), - ), - title: Text(L10n.of(context).sendVideo), - contentPadding: const EdgeInsets.all(0), - ), - ), - PopupMenuItem( - value: 'file', - child: ListTile( - leading: CircleAvatar( - backgroundColor: theme.colorScheme.onPrimaryContainer, - foregroundColor: theme.colorScheme.primaryContainer, - child: const Icon(Icons.attachment_outlined), - ), - title: Text(L10n.of(context).sendFile), - contentPadding: const EdgeInsets.all(0), - ), - ), - ], - ), - ), - if (PlatformInfos.isMobile) - AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - width: controller.sendController.text.isNotEmpty ? 0 : height, - height: height, - alignment: Alignment.center, - decoration: const BoxDecoration(), - clipBehavior: Clip.hardEdge, - child: PopupMenuButton( - icon: const Icon(Icons.camera_alt_outlined), - onSelected: controller.onAddPopupMenuButtonSelected, - iconColor: theme.colorScheme.onPrimaryContainer, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'camera-video', - child: ListTile( - leading: CircleAvatar( - backgroundColor: - theme.colorScheme.onPrimaryContainer, - foregroundColor: theme.colorScheme.primaryContainer, - child: const Icon(Icons.videocam_outlined), - ), - title: Text(L10n.of(context).recordAVideo), - contentPadding: const EdgeInsets.all(0), + Container( + height: height, + width: height, + alignment: Alignment.center, + child: IconButton( + tooltip: L10n.of(context).emojis, + color: theme.colorScheme.onPrimaryContainer, + icon: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.scaled, + fillColor: Colors.transparent, + child: child, + ); + }, + child: Icon( + controller.showEmojiPicker + ? Icons.keyboard + : Icons.add_reaction_outlined, + key: ValueKey(controller.showEmojiPicker), ), ), - PopupMenuItem( - value: 'camera', - child: ListTile( - leading: CircleAvatar( - backgroundColor: - theme.colorScheme.onPrimaryContainer, - foregroundColor: theme.colorScheme.primaryContainer, - child: const Icon(Icons.camera_alt_outlined), - ), - title: Text(L10n.of(context).takeAPhoto), - contentPadding: const EdgeInsets.all(0), - ), - ), - ], - ), - ), - Container( - height: height, - width: height, - alignment: Alignment.center, - child: IconButton( - tooltip: L10n.of(context).emojis, - color: theme.colorScheme.onPrimaryContainer, - icon: PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.scaled, - fillColor: Colors.transparent, - child: child, - ); - }, - child: Icon( - controller.showEmojiPicker - ? Icons.keyboard - : Icons.add_reaction_outlined, - key: ValueKey(controller.showEmojiPicker), + onPressed: controller.emojiPickerAction, ), ), - onPressed: controller.emojiPickerAction, - ), - ), - if (Matrix.of(context).isMultiAccount && - Matrix.of(context).hasComplexBundles && - Matrix.of(context).currentBundle!.length > 1) - Container( - width: height, - height: height, - alignment: Alignment.center, - child: _ChatAccountPicker(controller), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 0.0), - child: InputBar( - room: controller.room, - minLines: 1, - maxLines: 8, - autofocus: !PlatformInfos.isMobile, - keyboardType: TextInputType.multiline, - textInputAction: - AppConfig.sendOnEnter == true && PlatformInfos.isMobile + if (Matrix.of(context).isMultiAccount && + Matrix.of(context).hasComplexBundles && + Matrix.of(context).currentBundle!.length > 1) + Container( + height: height, + width: height, + alignment: Alignment.center, + child: _ChatAccountPicker(controller), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 0.0), + child: InputBar( + room: controller.room, + minLines: 1, + maxLines: 8, + autofocus: !PlatformInfos.isMobile, + keyboardType: TextInputType.multiline, + textInputAction: AppConfig.sendOnEnter == true && + PlatformInfos.isMobile ? TextInputAction.send : null, - onSubmitted: controller.onInputBarSubmitted, - onSubmitImage: controller.sendImageFromClipBoard, - focusNode: controller.inputFocus, - controller: controller.sendController, - decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - left: 6.0, - right: 6.0, - bottom: 6.0, - top: 3.0, + onSubmitted: controller.onInputBarSubmitted, + onSubmitImage: controller.sendImageFromClipBoard, + focusNode: controller.inputFocus, + controller: controller.sendController, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + left: 6.0, + right: 6.0, + bottom: 6.0, + top: 3.0, + ), + counter: const SizedBox.shrink(), + hintText: L10n.of(context).writeAMessage, + hintMaxLines: 1, + border: InputBorder.none, + enabledBorder: InputBorder.none, + filled: false, + ), + onChanged: controller.onInputBarChanged, ), - hintText: L10n.of(context).writeAMessage, - hintMaxLines: 1, - border: InputBorder.none, - enabledBorder: InputBorder.none, - filled: false, ), - onChanged: controller.onInputBarChanged, ), - ), - ), - Container( - height: height, - width: height, - alignment: Alignment.center, - child: PlatformInfos.platformCanRecord && - controller.sendController.text.isEmpty - ? FloatingActionButton.small( - tooltip: L10n.of(context).voiceMessage, - onPressed: controller.voiceMessageAction, - elevation: 0, - heroTag: null, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(height), - ), - backgroundColor: theme.bubbleColor, - foregroundColor: theme.onBubbleColor, - child: const Icon(Icons.mic_none_outlined), - ) - : FloatingActionButton.small( - tooltip: L10n.of(context).send, - onPressed: controller.send, - elevation: 0, - heroTag: null, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(height), - ), - backgroundColor: theme.bubbleColor, - foregroundColor: theme.onBubbleColor, - child: const Icon(Icons.send_outlined), - ), - ), - ], + Container( + height: height, + width: height, + alignment: Alignment.center, + child: PlatformInfos.platformCanRecord && + controller.sendController.text.isEmpty + ? IconButton( + tooltip: L10n.of(context).voiceMessage, + onPressed: () => + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + L10n.of(context) + .longPressToRecordVoiceMessage, + ), + ), + ), + onLongPress: () => recordingViewModel + .startRecording(controller.room), + style: IconButton.styleFrom( + backgroundColor: theme.bubbleColor, + foregroundColor: theme.onBubbleColor, + ), + icon: const Icon(Icons.mic_none_outlined), + ) + : IconButton( + tooltip: L10n.of(context).send, + onPressed: controller.send, + style: IconButton.styleFrom( + backgroundColor: theme.bubbleColor, + foregroundColor: theme.onBubbleColor, + ), + icon: const Icon(Icons.send_outlined), + ), + ), + ], + ); + }, ); } } @@ -368,6 +375,7 @@ class _ChatAccountPicker extends StatelessWidget { child: FutureBuilder( future: controller.sendingClient.fetchOwnProfile(), builder: (context, snapshot) => PopupMenuButton( + useRootNavigator: true, onSelected: (mxid) => _popupMenuButtonSelected(mxid, context), itemBuilder: (BuildContext context) => clients .map( @@ -399,4 +407,4 @@ class _ChatAccountPicker extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 696c4e6..4567fc4 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -15,7 +15,6 @@ import 'package:extera_next/pages/chat/chat_app_bar_title.dart'; import 'package:extera_next/pages/chat/chat_event_list.dart'; import 'package:extera_next/pages/chat/encryption_button.dart'; import 'package:extera_next/pages/chat/pinned_events.dart'; -import 'package:extera_next/pages/chat/reactions_picker.dart'; import 'package:extera_next/pages/chat/reply_display.dart'; import 'package:extera_next/utils/account_config.dart'; import 'package:extera_next/utils/localized_exception_extension.dart'; diff --git a/lib/pages/chat/recording_dialog.dart b/lib/pages/chat/recording_dialog.dart deleted file mode 100644 index 03ba75c..0000000 --- a/lib/pages/chat/recording_dialog.dart +++ /dev/null @@ -1,256 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import 'package:extera_next/generated/l10n/l10n.dart'; -import 'package:path/path.dart' as path_lib; -import 'package:path_provider/path_provider.dart'; -import 'package:record/record.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -import 'package:extera_next/config/app_config.dart'; -import 'package:extera_next/config/setting_keys.dart'; -import 'package:extera_next/utils/platform_infos.dart'; -import 'package:extera_next/widgets/matrix.dart'; -import 'events/audio_player.dart'; - -class RecordingDialog extends StatefulWidget { - const RecordingDialog({ - super.key, - }); - - @override - RecordingDialogState createState() => RecordingDialogState(); -} - -class RecordingDialogState extends State { - Timer? _recorderSubscription; - Duration _duration = Duration.zero; - - bool error = false; - - final _audioRecorder = AudioRecorder(); - final List amplitudeTimeline = []; - - String? fileName; - - Future startRecording() async { - final store = Matrix.of(context).store; - try { - final codec = kIsWeb - // Web seems to create webm instead of ogg when using opus encoder - // which does not play on iOS right now. So we use wav for now: - ? AudioEncoder.wav - // Everywhere else we use opus if supported by the platform: - : await _audioRecorder.isEncoderSupported(AudioEncoder.opus) - ? AudioEncoder.opus - : AudioEncoder.aacLc; - fileName = - 'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}'; - String? path; - if (!kIsWeb) { - final tempDir = await getTemporaryDirectory(); - path = path_lib.join(tempDir.path, fileName); - } - - final result = await _audioRecorder.hasPermission(); - if (result != true) { - setState(() => error = true); - return; - } - await WakelockPlus.enable(); - - await _audioRecorder.start( - RecordConfig( - bitRate: AppSettings.audioRecordingBitRate.getItem(store), - sampleRate: AppSettings.audioRecordingSamplingRate.getItem(store), - numChannels: AppSettings.audioRecordingNumChannels.getItem(store), - autoGain: AppSettings.audioRecordingAutoGain.getItem(store), - echoCancel: AppSettings.audioRecordingEchoCancel.getItem(store), - noiseSuppress: AppSettings.audioRecordingNoiseSuppress.getItem(store), - encoder: codec, - ), - path: path ?? '', - ); - setState(() => _duration = Duration.zero); - _recorderSubscription?.cancel(); - _recorderSubscription = - Timer.periodic(const Duration(milliseconds: 100), (_) async { - final amplitude = await _audioRecorder.getAmplitude(); - var value = 100 + amplitude.current * 2; - value = value < 1 ? 1 : value; - amplitudeTimeline.add(value); - setState(() { - _duration += const Duration(milliseconds: 100); - }); - }); - } catch (_) { - setState(() => error = true); - rethrow; - } - } - - @override - void initState() { - super.initState(); - startRecording(); - } - - @override - void dispose() { - WakelockPlus.disable(); - _recorderSubscription?.cancel(); - _audioRecorder.stop(); - super.dispose(); - } - - void _stopAndSend() async { - _recorderSubscription?.cancel(); - final path = await _audioRecorder.stop(); - - if (path == null) throw ('Recording failed!'); - const waveCount = AudioPlayerWidget.wavesCount; - final step = amplitudeTimeline.length < waveCount - ? 1 - : (amplitudeTimeline.length / waveCount).round(); - final waveform = []; - for (var i = 0; i < amplitudeTimeline.length; i += step) { - waveform.add((amplitudeTimeline[i] / 100 * 1024).round()); - } - Navigator.of(context, rootNavigator: false).pop( - RecordingResult( - path: path, - duration: _duration.inMilliseconds, - waveform: waveform, - fileName: fileName, - ), - ); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - const maxDecibalWidth = 64.0; - final time = - '${_duration.inMinutes.toString().padLeft(2, '0')}:${(_duration.inSeconds % 60).toString().padLeft(2, '0')}'; - final content = error - ? Text(L10n.of(context).oopsSomethingWentWrong) - : Row( - children: [ - Container( - width: 16, - height: 16, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(32), - color: Colors.red, - ), - ), - Expanded( - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: amplitudeTimeline.reversed - .take(26) - .toList() - .reversed - .map( - (amplitude) => Container( - margin: const EdgeInsets.only(left: 2), - width: 4, - decoration: BoxDecoration( - color: theme.colorScheme.primary, - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - ), - height: maxDecibalWidth * (amplitude / 100), - ), - ) - .toList(), - ), - ), - const SizedBox(width: 8), - SizedBox( - width: 48, - child: Text(time), - ), - ], - ); - if (PlatformInfos.isCupertinoStyle) { - return CupertinoAlertDialog( - content: content, - actions: [ - CupertinoDialogAction( - onPressed: () => Navigator.of(context, rootNavigator: false).pop(), - child: Text( - L10n.of(context).cancel, - style: TextStyle( - color: theme.textTheme.bodyMedium?.color?.withAlpha(150), - ), - ), - ), - if (error != true) - CupertinoDialogAction( - onPressed: _stopAndSend, - child: Text(L10n.of(context).send), - ), - ], - ); - } - return AlertDialog( - content: content, - actions: [ - TextButton( - onPressed: () => Navigator.of(context, rootNavigator: false).pop(), - child: Text( - L10n.of(context).cancel, - style: TextStyle( - color: theme.colorScheme.error, - ), - ), - ), - if (error != true) - TextButton( - onPressed: _stopAndSend, - child: Text(L10n.of(context).send), - ), - ], - ); - } -} - -class RecordingResult { - final String path; - final int duration; - final List waveform; - final String? fileName; - - const RecordingResult({ - required this.path, - required this.duration, - required this.waveform, - required this.fileName, - }); -} - -extension on AudioEncoder { - String get fileExtension { - switch (this) { - case AudioEncoder.aacLc: - case AudioEncoder.aacEld: - case AudioEncoder.aacHe: - return 'm4a'; - case AudioEncoder.opus: - return 'ogg'; - case AudioEncoder.wav: - return 'wav'; - case AudioEncoder.amrNb: - case AudioEncoder.amrWb: - case AudioEncoder.flac: - case AudioEncoder.pcm16bits: - throw UnsupportedError('Not yet used'); - } - } -} diff --git a/lib/pages/chat/recording_input_row.dart b/lib/pages/chat/recording_input_row.dart new file mode 100644 index 0000000..b90fb0b --- /dev/null +++ b/lib/pages/chat/recording_input_row.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +import 'package:extera_next/config/themes.dart'; + +import 'package:extera_next/generated/l10n/l10n.dart'; + +import 'package:extera_next/pages/chat/recording_view_model.dart'; + +class RecordingInputRow extends StatelessWidget { + final RecordingViewModelState state; + + final Future Function(String, int, List, String?) onSend; + + const RecordingInputRow({ + required this.state, + required this.onSend, + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + const maxDecibalWidth = 36.0; + + final time = + '${state.duration.inMinutes.toString().padLeft(2, '0')}:${(state.duration.inSeconds % 60).toString().padLeft(2, '0')}'; + + return Row( + children: [ + IconButton( + tooltip: L10n.of(context).cancel, + icon: const Icon(Icons.delete_outlined), + color: theme.colorScheme.error, + onPressed: state.cancel, + ), + if (state.isPaused) + IconButton( + tooltip: L10n.of(context).resume, + icon: const Icon(Icons.play_circle_outline_outlined), + onPressed: state.resume, + ) + else + IconButton( + tooltip: L10n.of(context).pause, + icon: const Icon(Icons.pause_circle_outline_outlined), + onPressed: state.pause, + ), + Text(time), + const SizedBox(width: 8), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + const width = 4; + + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: state.amplitudeTimeline.reversed + .take((constraints.maxWidth / (width + 2)).floor()) + .toList() + .reversed + .map( + (amplitude) => Container( + margin: const EdgeInsets.only(left: 2), + width: width.toDouble(), + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(2), + ), + height: maxDecibalWidth * (amplitude / 100), + ), + ) + .toList(), + ); + }, + ), + ), + IconButton( + style: IconButton.styleFrom( + disabledBackgroundColor: theme.bubbleColor.withAlpha(128), + backgroundColor: theme.bubbleColor, + foregroundColor: theme.onBubbleColor, + ), + tooltip: L10n.of(context).sendAudio, + icon: state.isSending + ? const SizedBox.square( + dimension: 24, + child: CircularProgressIndicator.adaptive(), + ) + : const Icon(Icons.send_outlined), + onPressed: state.isSending ? null : () => state.stopAndSend(onSend), + ), + ], + ); + } +} diff --git a/lib/pages/chat/recording_view_model.dart b/lib/pages/chat/recording_view_model.dart new file mode 100644 index 0000000..0054385 --- /dev/null +++ b/lib/pages/chat/recording_view_model.dart @@ -0,0 +1,230 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:matrix/matrix.dart'; +import 'package:path/path.dart' as path_lib; +import 'package:path_provider/path_provider.dart'; +import 'package:record/record.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + +import 'package:extera_next/config/setting_keys.dart'; +import 'package:extera_next/generated/l10n/l10n.dart'; +import 'package:extera_next/utils/platform_infos.dart'; +import 'package:extera_next/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:extera_next/widgets/matrix.dart'; +import 'events/audio_player.dart'; + +class RecordingViewModel extends StatefulWidget { + final Widget Function(BuildContext, RecordingViewModelState) builder; + + const RecordingViewModel({ + required this.builder, + super.key, + }); + + @override + RecordingViewModelState createState() => RecordingViewModelState(); +} + +class RecordingViewModelState extends State { + Timer? _recorderSubscription; + Duration duration = Duration.zero; + + bool error = false; + bool isSending = false; + + bool get isRecording => _audioRecorder != null; + + AudioRecorder? _audioRecorder; + final List amplitudeTimeline = []; + + String? fileName; + + bool isPaused = false; + + Future startRecording(Room room) async { + room.client.getConfig(); // Preload server file configuration. + if (PlatformInfos.isAndroid) { + final info = await DeviceInfoPlugin().androidInfo; + if (info.version.sdkInt < 19) { + showOkAlertDialog( + context: context, + title: L10n.of(context).unsupportedAndroidVersion, + message: L10n.of(context).unsupportedAndroidVersionLong, + okLabel: L10n.of(context).close, + ); + return; + } + } + + // TODO: add permission request + if (await AudioRecorder().hasPermission() == false) return; + + final store = Matrix.of(context).store; + + final audioRecorder = _audioRecorder ??= AudioRecorder(); + setState(() {}); + + try { + final codec = kIsWeb + // Web seems to create webm instead of ogg when using opus encoder + // which does not play on iOS right now. So we use wav for now: + ? AudioEncoder.wav + // Everywhere else we use opus if supported by the platform: + : await audioRecorder.isEncoderSupported(AudioEncoder.opus) + ? AudioEncoder.opus + : AudioEncoder.aacLc; + fileName = + 'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}'; + String? path; + if (!kIsWeb) { + final tempDir = await getTemporaryDirectory(); + path = path_lib.join(tempDir.path, fileName); + } + + final result = await audioRecorder.hasPermission(); + if (result != true) { + setState(() => error = true); + return; + } + await WakelockPlus.enable(); + + await audioRecorder.start( + RecordConfig( + bitRate: AppSettings.audioRecordingBitRate.getItem(store), + sampleRate: AppSettings.audioRecordingSamplingRate.getItem(store), + numChannels: AppSettings.audioRecordingNumChannels.getItem(store), + autoGain: AppSettings.audioRecordingAutoGain.getItem(store), + echoCancel: AppSettings.audioRecordingEchoCancel.getItem(store), + noiseSuppress: AppSettings.audioRecordingNoiseSuppress.getItem(store), + encoder: codec, + ), + path: path ?? '', + ); + setState(() => duration = Duration.zero); + _subscribe(); + } catch (_) { + setState(() => error = true); + rethrow; + } + } + + @override + void dispose() { + _reset(); + super.dispose(); + } + + void _subscribe() { + _recorderSubscription?.cancel(); + _recorderSubscription = + Timer.periodic(const Duration(milliseconds: 100), (_) async { + final amplitude = await _audioRecorder!.getAmplitude(); + var value = 100 + amplitude.current * 2; + value = value < 1 ? 1 : value; + amplitudeTimeline.add(value); + setState(() { + duration += const Duration(milliseconds: 100); + }); + }); + } + + void _reset() { + WakelockPlus.disable(); + _recorderSubscription?.cancel(); + _audioRecorder?.stop(); + _audioRecorder = null; + isSending = false; + error = false; + fileName = null; + duration = Duration.zero; + amplitudeTimeline.clear(); + isPaused = false; + } + + void cancel() { + setState(() { + _reset(); + }); + } + + void pause() { + _audioRecorder?.pause(); + _recorderSubscription?.cancel(); + setState(() { + isPaused = true; + }); + } + + void resume() { + _audioRecorder?.resume(); + _subscribe(); + setState(() { + isPaused = false; + }); + } + + void stopAndSend( + Future Function( + String path, + int duration, + List waveform, + String? fileName, + ) onSend, + ) async { + _recorderSubscription?.cancel(); + final path = await _audioRecorder?.stop(); + + if (path == null) throw ('Recording failed!'); + const waveCount = AudioPlayerWidget.wavesCount; + final step = amplitudeTimeline.length < waveCount + ? 1 + : (amplitudeTimeline.length / waveCount).round(); + final waveform = []; + for (var i = 0; i < amplitudeTimeline.length; i += step) { + waveform.add((amplitudeTimeline[i] / 100 * 1024).round()); + } + + setState(() { + isSending = true; + }); + try { + await onSend(path, duration.inMilliseconds, waveform, fileName); + } catch (e, s) { + Logs().e('Unable to send voice message', e, s); + setState(() { + isSending = false; + }); + return; + } + + cancel(); + } + + @override + Widget build(BuildContext context) => widget.builder(context, this); +} + +extension on AudioEncoder { + String get fileExtension { + switch (this) { + case AudioEncoder.aacLc: + case AudioEncoder.aacEld: + case AudioEncoder.aacHe: + return 'm4a'; + case AudioEncoder.opus: + return 'ogg'; + case AudioEncoder.wav: + return 'wav'; + case AudioEncoder.amrNb: + case AudioEncoder.amrWb: + case AudioEncoder.flac: + case AudioEncoder.pcm16bits: + throw UnsupportedError('Not yet used'); + } + } +} diff --git a/lib/pages/chat/recovered_event_dialog.dart b/lib/pages/chat/recovered_event_dialog.dart index abb52b9..9758ff6 100644 --- a/lib/pages/chat/recovered_event_dialog.dart +++ b/lib/pages/chat/recovered_event_dialog.dart @@ -1,6 +1,5 @@ import 'package:extera_next/config/themes.dart'; import 'package:extera_next/pages/chat/events/message.dart'; -import 'package:extera_next/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 49a81f5..f3ec688 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -7,7 +7,6 @@ import 'package:extera_next/generated/l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:mime/mime.dart'; -import 'package:extera_next/config/app_config.dart'; import 'package:extera_next/utils/localized_exception_extension.dart'; import 'package:extera_next/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:extera_next/utils/other_party_can_receive.dart'; diff --git a/lib/pages/chat/translated_event_dialog.dart b/lib/pages/chat/translated_event_dialog.dart index c1e412c..337701f 100644 --- a/lib/pages/chat/translated_event_dialog.dart +++ b/lib/pages/chat/translated_event_dialog.dart @@ -1,6 +1,5 @@ import 'package:extera_next/config/themes.dart'; import 'package:extera_next/pages/chat/events/message.dart'; -import 'package:extera_next/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index f930ed9..f5f4ccc 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -207,8 +207,9 @@ class ChatDetailsView extends StatelessWidget { ), ], ), - Divider(color: theme.dividerColor), - if (!room.canChangeStateEvent(EventTypes.RoomTopic)) + if (room.canChangeStateEvent(EventTypes.RoomTopic) || + room.topic.isNotEmpty) ...[ + Divider(color: theme.dividerColor), ListTile( title: Text( L10n.of(context).chatDescription, @@ -217,53 +218,46 @@ class ChatDetailsView extends StatelessWidget { fontWeight: FontWeight.bold, ), ), - ) - else + trailing: + room.canChangeStateEvent(EventTypes.RoomTopic) + ? IconButton( + onPressed: controller.setTopicAction, + tooltip: + L10n.of(context).setChatDescription, + icon: const Icon(Icons.edit_outlined), + ) + : null, + ), Padding( - padding: const EdgeInsets.all(16.0), - child: TextButton.icon( - onPressed: controller.setTopicAction, - label: Text(L10n.of(context).setChatDescription), - icon: const Icon(Icons.edit_outlined), - style: TextButton.styleFrom( - iconColor: - theme.colorScheme.onSecondaryContainer, - backgroundColor: - theme.colorScheme.secondaryContainer, - foregroundColor: - theme.colorScheme.onSecondaryContainer, + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + child: SelectableLinkify( + text: room.topic.isEmpty + ? L10n.of(context).noChatDescriptionYet + : room.topic, + textScaleFactor: + MediaQuery.textScalerOf(context).scale(1), + options: const LinkifyOptions(humanize: false), + linkStyle: const TextStyle( + color: Colors.blueAccent, + decorationColor: Colors.blueAccent, ), + style: TextStyle( + fontSize: 14, + fontStyle: room.topic.isEmpty + ? FontStyle.italic + : FontStyle.normal, + color: theme.textTheme.bodyMedium!.color, + decorationColor: + theme.textTheme.bodyMedium!.color, + ), + onOpen: (url) => + UrlLauncher(context, url.url).launchUrl(), ), ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - ), - child: SelectableLinkify( - text: room.topic.isEmpty - ? L10n.of(context).noChatDescriptionYet - : room.topic, - textScaleFactor: - MediaQuery.textScalerOf(context).scale(1), - options: const LinkifyOptions(humanize: false), - linkStyle: const TextStyle( - color: Colors.blueAccent, - decorationColor: Colors.blueAccent, - ), - style: TextStyle( - fontSize: 14, - fontStyle: room.topic.isEmpty - ? FontStyle.italic - : FontStyle.normal, - color: theme.textTheme.bodyMedium!.color, - decorationColor: - theme.textTheme.bodyMedium!.color, - ), - onOpen: (url) => - UrlLauncher(context, url.url).launchUrl(), - ), - ), - const SizedBox(height: 16), + const SizedBox(height: 16), + ], Divider(color: theme.dividerColor), ListTile( leading: CircleAvatar( @@ -366,4 +360,4 @@ class ChatDetailsView extends StatelessWidget { }, ); } -} +} \ No newline at end of file diff --git a/lib/widgets/avatar_page_header.dart b/lib/widgets/avatar_page_header.dart new file mode 100644 index 0000000..d62e541 --- /dev/null +++ b/lib/widgets/avatar_page_header.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; + +import 'package:extera_next/config/themes.dart'; + +class AvatarPageHeader extends StatelessWidget { + final Widget avatar; + + final void Function()? onAvatarEdit; + + final Widget? textButtonLeft, textButtonRight; + + final List iconButtons; + + const AvatarPageHeader({ + super.key, + required this.avatar, + this.onAvatarEdit, + this.iconButtons = const [], + this.textButtonLeft, + this.textButtonRight, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final onAvatarEdit = this.onAvatarEdit; + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: FluffyThemes.columnWidth), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 8.0, + children: [ + Stack( + children: [ + avatar, + if (onAvatarEdit != null) + Positioned( + bottom: 0, + right: 0, + child: FloatingActionButton.small( + elevation: 2, + onPressed: onAvatarEdit, + heroTag: null, + child: const Icon(Icons.camera_alt_outlined), + ), + ), + ], + ), + TextButtonTheme( + data: TextButtonThemeData( + style: TextButton.styleFrom( + disabledForegroundColor: theme.colorScheme.onSurface, + foregroundColor: theme.colorScheme.onSurface, + textStyle: const TextStyle(fontWeight: FontWeight.normal), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: LayoutBuilder( + builder: (context, constraints) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: constraints.maxWidth / 2, + ), + child: textButtonLeft, + ), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: constraints.maxWidth / 2, + ), + child: textButtonRight, + ), + ], + ); + }, + ), + ), + ), + IconButtonTheme( + data: IconButtonThemeData( + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.surfaceContainer, + iconSize: 24, + padding: const EdgeInsets.all(16), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: iconButtons, + ), + ), + const SizedBox(height: 0.0), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index a1b4569..05c5426 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -10,7 +10,7 @@ import 'package:extera_next/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog import 'package:extera_next/widgets/future_loading_dialog.dart'; import 'matrix.dart'; -enum ChatPopupMenuActions { details, mute, unmute, leave, search } +enum ChatPopupMenuActions { details, mute, unmute, emote, leave, search } class ChatSettingsPopupMenu extends StatefulWidget { final Room room; @@ -31,6 +31,17 @@ class ChatSettingsPopupMenuState extends State { super.dispose(); } + void goToEmoteSettings() async { + final room = widget.room; + if ((room.states['im.ponies.room_emotes'] ?? {}) + .keys + .any((String s) => s.isNotEmpty)) { + context.push('/rooms/${room.id}/details/multiple_emotes'); + } else { + context.push('/rooms/${room.id}/details/emotes'); + } + } + @override Widget build(BuildContext context) { notificationChangeSub ??= Matrix.of(context) @@ -73,6 +84,9 @@ class ChatSettingsPopupMenuState extends State { } } break; + case ChatPopupMenuActions.emote: + goToEmoteSettings(); + break; case ChatPopupMenuActions.mute: await showFutureLoadingDialog( context: context,