mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-20 02:59:26 +01:00
feat: new design
This commit is contained in:
parent
7e2148fa9b
commit
e2cdad27e0
@ -2613,5 +2613,9 @@
|
|||||||
"@zoomOut": {
|
"@zoomOut": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
}
|
},
|
||||||
|
"messageInfo": "Message info",
|
||||||
|
"time": "Time",
|
||||||
|
"messageType": "Message Type",
|
||||||
|
"sender": "Sender"
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,8 @@ abstract class AppConfig {
|
|||||||
static String _defaultHomeserver = 'matrix.org';
|
static String _defaultHomeserver = 'matrix.org';
|
||||||
static String get defaultHomeserver => _defaultHomeserver;
|
static String get defaultHomeserver => _defaultHomeserver;
|
||||||
static String jitsiInstance = 'https://meet.jit.si/';
|
static String jitsiInstance = 'https://meet.jit.si/';
|
||||||
static double fontSizeFactor = 1.0;
|
static double fontSizeFactor = 1;
|
||||||
|
static const double messageFontSize = 15.75;
|
||||||
static const bool allowOtherHomeservers = true;
|
static const bool allowOtherHomeservers = true;
|
||||||
static const bool enableRegistration = true;
|
static const bool enableRegistration = true;
|
||||||
static const Color primaryColor = Color(0xFF5625BA);
|
static const Color primaryColor = Color(0xFF5625BA);
|
||||||
@ -49,7 +50,7 @@ abstract class AppConfig {
|
|||||||
static const String emojiFontName = 'Noto Emoji';
|
static const String emojiFontName = 'Noto Emoji';
|
||||||
static const String emojiFontUrl =
|
static const String emojiFontUrl =
|
||||||
'https://github.com/googlefonts/noto-emoji/';
|
'https://github.com/googlefonts/noto-emoji/';
|
||||||
static const double borderRadius = 12.0;
|
static const double borderRadius = 16.0;
|
||||||
static const double columnWidth = 360.0;
|
static const double columnWidth = 360.0;
|
||||||
|
|
||||||
static void loadFromJson(Map<String, dynamic> json) {
|
static void loadFromJson(Map<String, dynamic> json) {
|
||||||
|
@ -62,13 +62,6 @@ abstract class FluffyThemes {
|
|||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
popupMenuTheme: PopupMenuThemeData(
|
popupMenuTheme: PopupMenuThemeData(
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@ -83,6 +76,9 @@ abstract class FluffyThemes {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
primary: AppConfig.primaryColor,
|
primary: AppConfig.primaryColor,
|
||||||
onPrimary: Colors.white,
|
onPrimary: Colors.white,
|
||||||
|
elevation: 6,
|
||||||
|
shadowColor: const Color(0x44000000),
|
||||||
|
minimumSize: const Size.fromHeight(48),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||||
),
|
),
|
||||||
@ -90,7 +86,8 @@ abstract class FluffyThemes {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
cardTheme: CardTheme(
|
cardTheme: CardTheme(
|
||||||
elevation: 4,
|
elevation: 6,
|
||||||
|
shadowColor: const Color(0x44000000),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||||
),
|
),
|
||||||
@ -109,7 +106,8 @@ abstract class FluffyThemes {
|
|||||||
fillColor: lighten(AppConfig.primaryColor, .51),
|
fillColor: lighten(AppConfig.primaryColor, .51),
|
||||||
),
|
),
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
elevation: 2,
|
elevation: 6,
|
||||||
|
shadowColor: Color(0x44000000),
|
||||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
titleTextStyle: TextStyle(
|
titleTextStyle: TextStyle(
|
||||||
@ -178,17 +176,11 @@ abstract class FluffyThemes {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
primary: AppConfig.primaryColor,
|
primary: AppConfig.primaryColor,
|
||||||
onPrimary: Colors.white,
|
onPrimary: Colors.white,
|
||||||
|
minimumSize: const Size.fromHeight(48),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||||
),
|
),
|
||||||
@ -197,7 +189,7 @@ abstract class FluffyThemes {
|
|||||||
),
|
),
|
||||||
snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating),
|
snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating),
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
elevation: 2,
|
elevation: 6,
|
||||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||||
backgroundColor: Color(0xff1D1D1D),
|
backgroundColor: Color(0xff1D1D1D),
|
||||||
titleTextStyle: TextStyle(
|
titleTextStyle: TextStyle(
|
||||||
|
@ -21,6 +21,7 @@ import 'package:vrouter/vrouter.dart';
|
|||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/pages/chat/chat_view.dart';
|
import 'package:fluffychat/pages/chat/chat_view.dart';
|
||||||
|
import 'package:fluffychat/pages/chat/event_info_dialog.dart';
|
||||||
import 'package:fluffychat/pages/chat/recording_dialog.dart';
|
import 'package:fluffychat/pages/chat/recording_dialog.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
@ -790,6 +791,9 @@ class ChatController extends State<Chat> {
|
|||||||
setState(() => inputText = text);
|
setState(() => inputText = text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showEventInfo([Event event]) =>
|
||||||
|
(event ?? selectedEvents.single).showInfoDialog(context);
|
||||||
|
|
||||||
void cancelReplyEventAction() => setState(() {
|
void cancelReplyEventAction() => setState(() {
|
||||||
if (editEvent != null) {
|
if (editEvent != null) {
|
||||||
inputText = sendController.text = pendingText;
|
inputText = sendController.text = pendingText;
|
||||||
|
@ -15,6 +15,7 @@ import 'package:fluffychat/config/themes.dart';
|
|||||||
import 'package:fluffychat/pages/chat/chat.dart';
|
import 'package:fluffychat/pages/chat/chat.dart';
|
||||||
import 'package:fluffychat/pages/chat/reactions_picker.dart';
|
import 'package:fluffychat/pages/chat/reactions_picker.dart';
|
||||||
import 'package:fluffychat/pages/chat/reply_display.dart';
|
import 'package:fluffychat/pages/chat/reply_display.dart';
|
||||||
|
import 'package:fluffychat/pages/chat/seen_by_row.dart';
|
||||||
import 'package:fluffychat/pages/chat/tombstone_display.dart';
|
import 'package:fluffychat/pages/chat/tombstone_display.dart';
|
||||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
||||||
@ -30,6 +31,8 @@ import 'chat_emoji_picker.dart';
|
|||||||
import 'chat_input_row.dart';
|
import 'chat_input_row.dart';
|
||||||
import 'events/message.dart';
|
import 'events/message.dart';
|
||||||
|
|
||||||
|
enum _EventContextAction { info, report }
|
||||||
|
|
||||||
class ChatView extends StatelessWidget {
|
class ChatView extends StatelessWidget {
|
||||||
final ChatController controller;
|
final ChatController controller;
|
||||||
|
|
||||||
@ -56,6 +59,7 @@ class ChatView extends StatelessWidget {
|
|||||||
showFutureLoadingDialog(
|
showFutureLoadingDialog(
|
||||||
context: context, future: () => controller.room.join());
|
context: context, future: () => controller.room.join());
|
||||||
}
|
}
|
||||||
|
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
|
||||||
|
|
||||||
return VWidgetGuard(
|
return VWidgetGuard(
|
||||||
onSystemPop: (redirector) async {
|
onSystemPop: (redirector) async {
|
||||||
@ -159,18 +163,50 @@ class ChatView extends StatelessWidget {
|
|||||||
tooltip: L10n.of(context).copy,
|
tooltip: L10n.of(context).copy,
|
||||||
onPressed: controller.copyEventsAction,
|
onPressed: controller.copyEventsAction,
|
||||||
),
|
),
|
||||||
if (controller.selectedEvents.length == 1)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.report_outlined),
|
|
||||||
tooltip: L10n.of(context).reportMessage,
|
|
||||||
onPressed: controller.reportEventAction,
|
|
||||||
),
|
|
||||||
if (controller.canRedactSelectedEvents)
|
if (controller.canRedactSelectedEvents)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete_outlined),
|
icon: const Icon(Icons.delete_outlined),
|
||||||
tooltip: L10n.of(context).redactMessage,
|
tooltip: L10n.of(context).redactMessage,
|
||||||
onPressed: controller.redactEventsAction,
|
onPressed: controller.redactEventsAction,
|
||||||
),
|
),
|
||||||
|
if (controller.selectedEvents.length == 1)
|
||||||
|
PopupMenuButton<_EventContextAction>(
|
||||||
|
onSelected: (action) {
|
||||||
|
switch (action) {
|
||||||
|
case _EventContextAction.info:
|
||||||
|
controller.showEventInfo();
|
||||||
|
controller.clearSelectedEvents();
|
||||||
|
break;
|
||||||
|
case _EventContextAction.report:
|
||||||
|
controller.reportEventAction();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: _EventContextAction.info,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.info_outlined),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(L10n.of(context).messageInfo),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: _EventContextAction.report,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.report_outlined),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(L10n.of(context).reportMessage),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
: <Widget>[
|
: <Widget>[
|
||||||
if (controller.room.canSendDefaultStates)
|
if (controller.room.canSendDefaultStates)
|
||||||
@ -197,6 +233,8 @@ class ChatView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
backgroundColor: Theme.of(context).primaryColor.withAlpha(
|
||||||
|
Theme.of(context).brightness == Brightness.light ? 15 : 70),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
if (Matrix.of(context).wallpaper != null)
|
if (Matrix.of(context).wallpaper != null)
|
||||||
@ -206,199 +244,155 @@ class ChatView extends StatelessWidget {
|
|||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
SafeArea(
|
Column(
|
||||||
child: Column(
|
children: <Widget>[
|
||||||
children: <Widget>[
|
TombstoneDisplay(controller),
|
||||||
TombstoneDisplay(controller),
|
Expanded(
|
||||||
Expanded(
|
child: FutureBuilder<bool>(
|
||||||
child: FutureBuilder<bool>(
|
future: controller.getTimeline(),
|
||||||
future: controller.getTimeline(),
|
builder: (BuildContext context, snapshot) {
|
||||||
builder: (BuildContext context, snapshot) {
|
if (controller.timeline == null) {
|
||||||
if (controller.timeline == null) {
|
return const Center(
|
||||||
return const Center(
|
child: CircularProgressIndicator.adaptive(
|
||||||
child: CircularProgressIndicator.adaptive(
|
strokeWidth: 2),
|
||||||
strokeWidth: 2),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a map of eventId --> index to greatly improve performance of
|
|
||||||
// ListView's findChildIndexCallback
|
|
||||||
final thisEventsKeyMap = <String, int>{};
|
|
||||||
for (var i = 0;
|
|
||||||
i < controller.filteredEvents.length;
|
|
||||||
i++) {
|
|
||||||
thisEventsKeyMap[
|
|
||||||
controller.filteredEvents[i].eventId] = i;
|
|
||||||
}
|
|
||||||
final seenByText =
|
|
||||||
controller.room.getLocalizedSeenByText(
|
|
||||||
context,
|
|
||||||
controller.timeline,
|
|
||||||
controller.filteredEvents,
|
|
||||||
controller.unfolded,
|
|
||||||
);
|
);
|
||||||
return ListView.custom(
|
}
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 16,
|
// create a map of eventId --> index to greatly improve performance of
|
||||||
bottom: 4,
|
// ListView's findChildIndexCallback
|
||||||
),
|
final thisEventsKeyMap = <String, int>{};
|
||||||
reverse: true,
|
for (var i = 0;
|
||||||
controller: controller.scrollController,
|
i < controller.filteredEvents.length;
|
||||||
keyboardDismissBehavior: PlatformInfos.isIOS
|
i++) {
|
||||||
? ScrollViewKeyboardDismissBehavior.onDrag
|
thisEventsKeyMap[
|
||||||
: ScrollViewKeyboardDismissBehavior.manual,
|
controller.filteredEvents[i].eventId] = i;
|
||||||
childrenDelegate: SliverChildBuilderDelegate(
|
}
|
||||||
(BuildContext context, int i) {
|
return ListView.custom(
|
||||||
return i == controller.filteredEvents.length + 1
|
padding: const EdgeInsets.only(
|
||||||
? controller.timeline.isRequestingHistory
|
top: 16,
|
||||||
? const Center(
|
bottom: 4,
|
||||||
child: CircularProgressIndicator
|
),
|
||||||
.adaptive(strokeWidth: 2),
|
reverse: true,
|
||||||
)
|
controller: controller.scrollController,
|
||||||
: controller.canLoadMore
|
keyboardDismissBehavior: PlatformInfos.isIOS
|
||||||
? Center(
|
? ScrollViewKeyboardDismissBehavior.onDrag
|
||||||
child: OutlinedButton(
|
: ScrollViewKeyboardDismissBehavior.manual,
|
||||||
style:
|
childrenDelegate: SliverChildBuilderDelegate(
|
||||||
OutlinedButton.styleFrom(
|
(BuildContext context, int i) {
|
||||||
backgroundColor: Theme.of(
|
return i == controller.filteredEvents.length + 1
|
||||||
context)
|
? controller.timeline.isRequestingHistory
|
||||||
.scaffoldBackgroundColor,
|
? const Center(
|
||||||
),
|
child: CircularProgressIndicator
|
||||||
onPressed:
|
.adaptive(strokeWidth: 2),
|
||||||
controller.requestHistory,
|
)
|
||||||
child: Text(L10n.of(context)
|
: controller.canLoadMore
|
||||||
.loadMore),
|
? Center(
|
||||||
|
child: OutlinedButton(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(
|
||||||
|
context)
|
||||||
|
.scaffoldBackgroundColor,
|
||||||
),
|
),
|
||||||
)
|
onPressed:
|
||||||
: Container()
|
controller.requestHistory,
|
||||||
: i == 0
|
child: Text(
|
||||||
? AnimatedContainer(
|
L10n.of(context).loadMore),
|
||||||
height: seenByText.isEmpty ? 0 : 24,
|
|
||||||
duration: seenByText.isEmpty
|
|
||||||
? const Duration(
|
|
||||||
milliseconds: 0)
|
|
||||||
: const Duration(
|
|
||||||
milliseconds: 300),
|
|
||||||
alignment: controller.filteredEvents
|
|
||||||
.isNotEmpty &&
|
|
||||||
controller.filteredEvents
|
|
||||||
.first.senderId ==
|
|
||||||
client.userID
|
|
||||||
? Alignment.topRight
|
|
||||||
: Alignment.topLeft,
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 8,
|
|
||||||
right: 8,
|
|
||||||
bottom: 8,
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(
|
|
||||||
horizontal: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.scaffoldBackgroundColor
|
|
||||||
.withOpacity(0.8),
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(4),
|
|
||||||
),
|
),
|
||||||
child: Text(
|
)
|
||||||
seenByText,
|
: Container()
|
||||||
maxLines: 1,
|
: i == 0
|
||||||
overflow: TextOverflow.ellipsis,
|
? SeenByRow(controller)
|
||||||
style: TextStyle(
|
: AutoScrollTag(
|
||||||
color: Theme.of(context)
|
key: ValueKey(controller
|
||||||
.colorScheme
|
.filteredEvents[i - 1].eventId),
|
||||||
.secondary,
|
index: i - 1,
|
||||||
),
|
controller:
|
||||||
),
|
controller.scrollController,
|
||||||
),
|
child: Swipeable(
|
||||||
)
|
|
||||||
: AutoScrollTag(
|
|
||||||
key: ValueKey(controller
|
key: ValueKey(controller
|
||||||
.filteredEvents[i - 1].eventId),
|
.filteredEvents[i - 1].eventId),
|
||||||
index: i - 1,
|
background: const Padding(
|
||||||
controller:
|
padding: EdgeInsets.symmetric(
|
||||||
controller.scrollController,
|
horizontal: 12.0),
|
||||||
child: Swipeable(
|
child: Center(
|
||||||
key: ValueKey(controller
|
child:
|
||||||
.filteredEvents[i - 1]
|
Icon(Icons.reply_outlined),
|
||||||
.eventId),
|
|
||||||
background: const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 12.0),
|
|
||||||
child: Center(
|
|
||||||
child: Icon(
|
|
||||||
Icons.reply_outlined),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
direction:
|
|
||||||
SwipeDirection.endToStart,
|
|
||||||
onSwipe: (direction) =>
|
|
||||||
controller.replyAction(
|
|
||||||
replyTo: controller
|
|
||||||
.filteredEvents[
|
|
||||||
i - 1]),
|
|
||||||
child: Message(
|
|
||||||
controller
|
|
||||||
.filteredEvents[i - 1],
|
|
||||||
onAvatarTab: (Event event) =>
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (c) =>
|
|
||||||
UserBottomSheet(
|
|
||||||
user: event.sender,
|
|
||||||
outerContext: context,
|
|
||||||
onMention: () => controller
|
|
||||||
.sendController
|
|
||||||
.text +=
|
|
||||||
'${event.sender.mention} ',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
unfold: controller.unfold,
|
|
||||||
onSelect: controller
|
|
||||||
.onSelectMessage,
|
|
||||||
scrollToEventId: (String eventId) =>
|
|
||||||
controller.scrollToEventId(
|
|
||||||
eventId),
|
|
||||||
longPressSelect: controller
|
|
||||||
.selectedEvents.isEmpty,
|
|
||||||
selected: controller
|
|
||||||
.selectedEvents
|
|
||||||
.contains(
|
|
||||||
controller.filteredEvents[
|
|
||||||
i - 1]),
|
|
||||||
timeline: controller.timeline,
|
|
||||||
nextEvent: i >= 2
|
|
||||||
? controller.filteredEvents[i - 2]
|
|
||||||
: null),
|
|
||||||
),
|
),
|
||||||
);
|
direction:
|
||||||
},
|
SwipeDirection.endToStart,
|
||||||
childCount: controller.filteredEvents.length + 2,
|
onSwipe: (direction) =>
|
||||||
findChildIndexCallback: (key) =>
|
controller.replyAction(
|
||||||
controller.findChildIndexCallback(
|
replyTo: controller
|
||||||
key, thisEventsKeyMap),
|
.filteredEvents[i - 1]),
|
||||||
),
|
child: Message(
|
||||||
);
|
controller
|
||||||
},
|
.filteredEvents[i - 1],
|
||||||
),
|
onInfoTab:
|
||||||
|
controller.showEventInfo,
|
||||||
|
onAvatarTab: (Event event) =>
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (c) =>
|
||||||
|
UserBottomSheet(
|
||||||
|
user: event.sender,
|
||||||
|
outerContext: context,
|
||||||
|
onMention: () => controller
|
||||||
|
.sendController
|
||||||
|
.text +=
|
||||||
|
'${event.sender.mention} ',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
unfold: controller.unfold,
|
||||||
|
onSelect:
|
||||||
|
controller.onSelectMessage,
|
||||||
|
scrollToEventId: (String eventId) =>
|
||||||
|
controller.scrollToEventId(
|
||||||
|
eventId),
|
||||||
|
longPressSelect: controller
|
||||||
|
.selectedEvents.isEmpty,
|
||||||
|
selected: controller
|
||||||
|
.selectedEvents
|
||||||
|
.contains(controller
|
||||||
|
.filteredEvents[i - 1]),
|
||||||
|
timeline: controller.timeline,
|
||||||
|
nextEvent: i <
|
||||||
|
controller
|
||||||
|
.filteredEvents
|
||||||
|
.length
|
||||||
|
? controller.filteredEvents[i]
|
||||||
|
: null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: controller.filteredEvents.length + 2,
|
||||||
|
findChildIndexCallback: (key) => controller
|
||||||
|
.findChildIndexCallback(key, thisEventsKeyMap),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
if (controller.showScrollDownButton)
|
),
|
||||||
const Divider(
|
if (controller.room.canSendDefaultMessages &&
|
||||||
height: 1,
|
controller.room.membership == Membership.join)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: bottomSheetPadding,
|
||||||
|
left: bottomSheetPadding,
|
||||||
|
right: bottomSheetPadding,
|
||||||
),
|
),
|
||||||
if (controller.room.canSendDefaultMessages &&
|
child: Material(
|
||||||
controller.room.membership == Membership.join)
|
borderRadius: const BorderRadius.only(
|
||||||
Padding(
|
bottomLeft: Radius.circular(AppConfig.borderRadius),
|
||||||
padding: EdgeInsets.all(
|
bottomRight: Radius.circular(AppConfig.borderRadius),
|
||||||
FluffyThemes.isColumnMode(context) ? 16.0 : 8.0),
|
),
|
||||||
child: Material(
|
elevation: 6,
|
||||||
borderRadius:
|
shadowColor: Theme.of(context)
|
||||||
BorderRadius.circular(AppConfig.borderRadius),
|
.secondaryHeaderColor
|
||||||
elevation: 7,
|
.withAlpha(100),
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
color: Theme.of(context).appBarTheme.backgroundColor,
|
color: Theme.of(context).backgroundColor,
|
||||||
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -411,8 +405,8 @@ class ChatView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
98
lib/pages/chat/event_info_dialog.dart
Normal file
98
lib/pages/chat/event_info_dialog.dart
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||||
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
|
|
||||||
|
extension EventInfoDialogExtension on Event {
|
||||||
|
void showInfoDialog(BuildContext context) => showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
EventInfoDialog(l10n: L10n.of(context), event: this),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventInfoDialog extends StatelessWidget {
|
||||||
|
final Event event;
|
||||||
|
final L10n l10n;
|
||||||
|
const EventInfoDialog({
|
||||||
|
@required this.event,
|
||||||
|
@required this.l10n,
|
||||||
|
Key key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
String get prettyJson {
|
||||||
|
const JsonDecoder decoder = JsonDecoder();
|
||||||
|
const JsonEncoder encoder = JsonEncoder.withIndent(' ');
|
||||||
|
final object = decoder.convert(jsonEncode(event.toJson()));
|
||||||
|
return encoder.convert(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(L10n.of(context).messageInfo),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_downward_outlined),
|
||||||
|
onPressed: Navigator.of(context, rootNavigator: false).pop,
|
||||||
|
tooltip: L10n.of(context).close,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading:
|
||||||
|
Avatar(event.sender.avatarUrl, event.sender.calcDisplayname()),
|
||||||
|
title: Text(L10n.of(context).sender),
|
||||||
|
subtitle:
|
||||||
|
Text('${event.sender.calcDisplayname()} <${event.senderId}>'),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10n.of(context).time),
|
||||||
|
subtitle: Text(event.originServerTs.localizedTime(context)),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10n.of(context).messageType),
|
||||||
|
subtitle: Text(event.humanreadableType),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10n.of(context).sourceCode),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Text(
|
||||||
|
prettyJson,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'Roboto-Mono',
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on Event {
|
||||||
|
String get humanreadableType {
|
||||||
|
if (type == EventTypes.Message) {
|
||||||
|
return messageType.split('m.').last;
|
||||||
|
}
|
||||||
|
if (type.startsWith('m.room.')) {
|
||||||
|
return type.split('m.room.').last;
|
||||||
|
}
|
||||||
|
if (type.startsWith('m.')) {
|
||||||
|
return type.split('m.').last;
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,6 @@ import 'package:matrix/matrix.dart';
|
|||||||
|
|
||||||
import 'package:fluffychat/config/themes.dart';
|
import 'package:fluffychat/config/themes.dart';
|
||||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart';
|
|
||||||
import 'package:fluffychat/utils/string_color.dart';
|
import 'package:fluffychat/utils/string_color.dart';
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
@ -20,6 +19,7 @@ class Message extends StatelessWidget {
|
|||||||
final Event nextEvent;
|
final Event nextEvent;
|
||||||
final void Function(Event) onSelect;
|
final void Function(Event) onSelect;
|
||||||
final void Function(Event) onAvatarTab;
|
final void Function(Event) onAvatarTab;
|
||||||
|
final void Function(Event) onInfoTab;
|
||||||
final void Function(String) scrollToEventId;
|
final void Function(String) scrollToEventId;
|
||||||
final void Function(String) unfold;
|
final void Function(String) unfold;
|
||||||
final bool longPressSelect;
|
final bool longPressSelect;
|
||||||
@ -30,6 +30,7 @@ class Message extends StatelessWidget {
|
|||||||
{this.nextEvent,
|
{this.nextEvent,
|
||||||
this.longPressSelect,
|
this.longPressSelect,
|
||||||
this.onSelect,
|
this.onSelect,
|
||||||
|
this.onInfoTab,
|
||||||
this.onAvatarTab,
|
this.onAvatarTab,
|
||||||
this.scrollToEventId,
|
this.scrollToEventId,
|
||||||
@required this.unfold,
|
@required this.unfold,
|
||||||
@ -57,12 +58,19 @@ class Message extends StatelessWidget {
|
|||||||
final client = Matrix.of(context).client;
|
final client = Matrix.of(context).client;
|
||||||
final ownMessage = event.senderId == client.userID;
|
final ownMessage = event.senderId == client.userID;
|
||||||
final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft;
|
final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft;
|
||||||
var color = Theme.of(context).secondaryHeaderColor;
|
var color = Theme.of(context).scaffoldBackgroundColor;
|
||||||
|
final displayTime = event.type == EventTypes.RoomCreate ||
|
||||||
|
nextEvent == null ||
|
||||||
|
!event.originServerTs.sameEnvironment(nextEvent.originServerTs);
|
||||||
final sameSender = nextEvent != null &&
|
final sameSender = nextEvent != null &&
|
||||||
[EventTypes.Message, EventTypes.Sticker].contains(nextEvent.type)
|
[
|
||||||
? nextEvent.sender.id == event.sender.id
|
EventTypes.Message,
|
||||||
|
EventTypes.Sticker,
|
||||||
|
EventTypes.Encrypted,
|
||||||
|
].contains(nextEvent.type)
|
||||||
|
? nextEvent.sender.id == event.sender.id && !displayTime
|
||||||
: false;
|
: false;
|
||||||
var textColor = ownMessage
|
final textColor = ownMessage
|
||||||
? Colors.white
|
? Colors.white
|
||||||
: Theme.of(context).brightness == Brightness.dark
|
: Theme.of(context).brightness == Brightness.dark
|
||||||
? Colors.white
|
? Colors.white
|
||||||
@ -71,148 +79,182 @@ class Message extends StatelessWidget {
|
|||||||
ownMessage ? MainAxisAlignment.end : MainAxisAlignment.start;
|
ownMessage ? MainAxisAlignment.end : MainAxisAlignment.start;
|
||||||
|
|
||||||
final displayEvent = event.getDisplayEvent(timeline);
|
final displayEvent = event.getDisplayEvent(timeline);
|
||||||
|
final borderRadius = BorderRadius.only(
|
||||||
|
topLeft: !ownMessage
|
||||||
|
? const Radius.circular(2)
|
||||||
|
: const Radius.circular(AppConfig.borderRadius),
|
||||||
|
topRight: ownMessage
|
||||||
|
? const Radius.circular(2)
|
||||||
|
: const Radius.circular(AppConfig.borderRadius),
|
||||||
|
bottomLeft: const Radius.circular(AppConfig.borderRadius),
|
||||||
|
bottomRight: const Radius.circular(AppConfig.borderRadius),
|
||||||
|
);
|
||||||
|
|
||||||
if (event.showThumbnail) {
|
if (ownMessage) {
|
||||||
color = Colors.transparent;
|
|
||||||
textColor = Theme.of(context).textTheme.bodyText2.color;
|
|
||||||
} else if (ownMessage) {
|
|
||||||
color = displayEvent.status.isError
|
color = displayEvent.status.isError
|
||||||
? Colors.redAccent
|
? Colors.redAccent
|
||||||
: Theme.of(context).primaryColor;
|
: Theme.of(context).primaryColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
final rowChildren = <Widget>[
|
final rowChildren = <Widget>[
|
||||||
|
sameSender || ownMessage
|
||||||
|
? SizedBox(
|
||||||
|
width: Avatar.defaultSize,
|
||||||
|
height: Avatar.defaultSize,
|
||||||
|
child: event.status == EventStatus.sending
|
||||||
|
? const Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child:
|
||||||
|
CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
: Avatar(
|
||||||
|
event.sender.avatarUrl,
|
||||||
|
event.sender.calcDisplayname(),
|
||||||
|
onTap: () => onAvatarTab(event),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Column(
|
||||||
alignment: alignment,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Material(
|
children: [
|
||||||
color: color,
|
if (!ownMessage && !sameSender)
|
||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
Padding(
|
||||||
child: InkWell(
|
padding: const EdgeInsets.only(left: 8.0, bottom: 4),
|
||||||
onHover: (b) => useMouse = true,
|
child: event.room.isDirectChat
|
||||||
onTap: !useMouse && longPressSelect
|
? const SizedBox(height: 12)
|
||||||
? () => null
|
: Text(
|
||||||
: () => onSelect(event),
|
event.sender.calcDisplayname(),
|
||||||
onLongPress: !longPressSelect ? null : () => onSelect(event),
|
style: TextStyle(
|
||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
fontSize: 12,
|
||||||
child: Container(
|
fontWeight: FontWeight.bold,
|
||||||
decoration: BoxDecoration(
|
color: event.sender.calcDisplayname().color,
|
||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
|
||||||
),
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(vertical: 6, horizontal: 10),
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
maxWidth: FluffyThemes.columnWidth * 1.5),
|
|
||||||
child: Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
if (event.relationshipType == RelationshipTypes.reply)
|
|
||||||
FutureBuilder<Event>(
|
|
||||||
future: event.getReplyEvent(timeline),
|
|
||||||
builder: (BuildContext context, snapshot) {
|
|
||||||
final replyEvent = snapshot.hasData
|
|
||||||
? snapshot.data
|
|
||||||
: Event(
|
|
||||||
eventId: event.relationshipEventId,
|
|
||||||
content: {
|
|
||||||
'msgtype': 'm.text',
|
|
||||||
'body': '...'
|
|
||||||
},
|
|
||||||
senderId: event.senderId,
|
|
||||||
type: 'm.room.message',
|
|
||||||
room: event.room,
|
|
||||||
status: EventStatus.sent,
|
|
||||||
originServerTs: DateTime.now(),
|
|
||||||
);
|
|
||||||
return InkWell(
|
|
||||||
onTap: () {
|
|
||||||
if (scrollToEventId != null) {
|
|
||||||
scrollToEventId(replyEvent.eventId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: AbsorbPointer(
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.symmetric(
|
|
||||||
vertical: 4.0),
|
|
||||||
child: ReplyContent(replyEvent,
|
|
||||||
lightText: ownMessage,
|
|
||||||
timeline: timeline),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MessageContent(
|
|
||||||
displayEvent,
|
|
||||||
textColor: textColor,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
),
|
||||||
Opacity(
|
),
|
||||||
opacity: 0,
|
Container(
|
||||||
child: _MetaRow(
|
alignment: alignment,
|
||||||
event, // meta information should be from the unedited event
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
ownMessage,
|
child: Material(
|
||||||
textColor,
|
color: color,
|
||||||
timeline,
|
elevation: 6,
|
||||||
displayEvent,
|
shadowColor:
|
||||||
),
|
Theme.of(context).secondaryHeaderColor.withAlpha(100),
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
child: InkWell(
|
||||||
|
onHover: (b) => useMouse = true,
|
||||||
|
onTap: !useMouse && longPressSelect
|
||||||
|
? () => null
|
||||||
|
: () => onSelect(event),
|
||||||
|
onLongPress: !longPressSelect ? null : () => onSelect(event),
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(AppConfig.borderRadius),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: FluffyThemes.columnWidth * 1.5),
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
if (event.relationshipType ==
|
||||||
|
RelationshipTypes.reply)
|
||||||
|
FutureBuilder<Event>(
|
||||||
|
future: event.getReplyEvent(timeline),
|
||||||
|
builder: (BuildContext context, snapshot) {
|
||||||
|
final replyEvent = snapshot.hasData
|
||||||
|
? snapshot.data
|
||||||
|
: Event(
|
||||||
|
eventId: event.relationshipEventId,
|
||||||
|
content: {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': '...'
|
||||||
|
},
|
||||||
|
senderId: event.senderId,
|
||||||
|
type: 'm.room.message',
|
||||||
|
room: event.room,
|
||||||
|
status: EventStatus.sent,
|
||||||
|
originServerTs: DateTime.now(),
|
||||||
|
);
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (scrollToEventId != null) {
|
||||||
|
scrollToEventId(replyEvent.eventId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: AbsorbPointer(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 4.0),
|
||||||
|
child: ReplyContent(replyEvent,
|
||||||
|
lightText: ownMessage,
|
||||||
|
timeline: timeline),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MessageContent(
|
||||||
|
displayEvent,
|
||||||
|
textColor: textColor,
|
||||||
|
onInfoTab: onInfoTab,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Positioned(
|
),
|
||||||
bottom: 0,
|
|
||||||
right: ownMessage ? 0 : null,
|
|
||||||
left: !ownMessage ? 0 : null,
|
|
||||||
child: _MetaRow(
|
|
||||||
event,
|
|
||||||
ownMessage,
|
|
||||||
textColor,
|
|
||||||
timeline,
|
|
||||||
displayEvent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
final avatarOrSizedBox = sameSender
|
|
||||||
? const SizedBox(width: Avatar.defaultSize)
|
|
||||||
: Avatar(
|
|
||||||
event.sender.avatarUrl,
|
|
||||||
event.sender.calcDisplayname(),
|
|
||||||
onTap: () => onAvatarTab(event),
|
|
||||||
);
|
|
||||||
if (ownMessage) {
|
|
||||||
rowChildren.add(avatarOrSizedBox);
|
|
||||||
} else {
|
|
||||||
rowChildren.insert(0, avatarOrSizedBox);
|
|
||||||
}
|
|
||||||
final row = Row(
|
final row = Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: rowMainAxisAlignment,
|
mainAxisAlignment: rowMainAxisAlignment,
|
||||||
children: rowChildren,
|
children: rowChildren,
|
||||||
);
|
);
|
||||||
Widget container;
|
Widget container;
|
||||||
if (event.hasAggregatedEvents(timeline, RelationshipTypes.reaction)) {
|
if (event.hasAggregatedEvents(timeline, RelationshipTypes.reaction) ||
|
||||||
|
displayTime) {
|
||||||
container = Column(
|
container = Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment:
|
crossAxisAlignment:
|
||||||
ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
if (displayTime)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Center(
|
||||||
|
child: Material(
|
||||||
|
color: Theme.of(context).backgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: Text(
|
||||||
|
event.originServerTs.localizedTime(context),
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
row,
|
row,
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: 4.0,
|
top: 4.0,
|
||||||
left: (ownMessage ? 0 : Avatar.defaultSize) + 12.0,
|
left: (ownMessage ? 0 : Avatar.defaultSize) + 12.0,
|
||||||
right: (ownMessage ? Avatar.defaultSize : 0) + 12.0,
|
right: 12.0,
|
||||||
),
|
),
|
||||||
child: MessageReactions(event, timeline),
|
child: MessageReactions(event, timeline),
|
||||||
),
|
),
|
||||||
@ -238,64 +280,3 @@ class Message extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MetaRow extends StatelessWidget {
|
|
||||||
final Event event;
|
|
||||||
final bool ownMessage;
|
|
||||||
final Color color;
|
|
||||||
final Timeline timeline;
|
|
||||||
final Event displayEvent;
|
|
||||||
|
|
||||||
const _MetaRow(
|
|
||||||
this.event, this.ownMessage, this.color, this.timeline, this.displayEvent,
|
|
||||||
{Key key})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final displayname = event.sender.calcDisplayname();
|
|
||||||
final showDisplayname =
|
|
||||||
!ownMessage && event.senderId != event.room.directChatMatrixID;
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
if (showDisplayname)
|
|
||||||
Text(
|
|
||||||
displayname,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 10 * AppConfig.fontSizeFactor,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: (Theme.of(context).brightness == Brightness.light
|
|
||||||
? displayname.darkColor
|
|
||||||
: displayname.lightColor)
|
|
||||||
.withAlpha(200),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (showDisplayname) const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
event.originServerTs.localizedTime(context),
|
|
||||||
style: TextStyle(
|
|
||||||
color: color.withAlpha(180),
|
|
||||||
fontSize: 10 * AppConfig.fontSizeFactor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (event.hasAggregatedEvents(timeline, RelationshipTypes.edit))
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 2.0),
|
|
||||||
child: Icon(
|
|
||||||
Icons.edit_outlined,
|
|
||||||
size: 12 * AppConfig.fontSizeFactor,
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (ownMessage) const SizedBox(width: 2),
|
|
||||||
if (ownMessage)
|
|
||||||
Icon(
|
|
||||||
displayEvent.statusIcon,
|
|
||||||
size: 14 * AppConfig.fontSizeFactor,
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -25,8 +25,10 @@ import 'sticker.dart';
|
|||||||
class MessageContent extends StatelessWidget {
|
class MessageContent extends StatelessWidget {
|
||||||
final Event event;
|
final Event event;
|
||||||
final Color textColor;
|
final Color textColor;
|
||||||
|
final void Function(Event) onInfoTab;
|
||||||
|
|
||||||
const MessageContent(this.event, {Key key, this.textColor}) : super(key: key);
|
const MessageContent(this.event, {this.onInfoTab, Key key, this.textColor})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
void _verifyOrRequestKey(BuildContext context) async {
|
void _verifyOrRequestKey(BuildContext context) async {
|
||||||
if (event.content['can_request_session'] != true) {
|
if (event.content['can_request_session'] != true) {
|
||||||
@ -72,8 +74,7 @@ class MessageContent extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final fontSize =
|
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||||
DefaultTextStyle.of(context).style.fontSize * AppConfig.fontSizeFactor;
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case EventTypes.Message:
|
case EventTypes.Message:
|
||||||
case EventTypes.Encrypted:
|
case EventTypes.Encrypted:
|
||||||
@ -163,14 +164,11 @@ class MessageContent extends StatelessWidget {
|
|||||||
continue textmessage;
|
continue textmessage;
|
||||||
case MessageTypes.BadEncrypted:
|
case MessageTypes.BadEncrypted:
|
||||||
case EventTypes.Encrypted:
|
case EventTypes.Encrypted:
|
||||||
return ElevatedButton.icon(
|
return _ButtonContent(
|
||||||
style: ElevatedButton.styleFrom(
|
textColor: textColor,
|
||||||
primary: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
onPrimary: Theme.of(context).textTheme.bodyText1.color,
|
|
||||||
),
|
|
||||||
onPressed: () => _verifyOrRequestKey(context),
|
onPressed: () => _verifyOrRequestKey(context),
|
||||||
icon: const Icon(Icons.lock_outline),
|
icon: const Icon(Icons.lock_outline),
|
||||||
label: Text(L10n.of(context).encrypted),
|
label: L10n.of(context).encrypted,
|
||||||
);
|
);
|
||||||
case MessageTypes.Location:
|
case MessageTypes.Location:
|
||||||
final geoUri =
|
final geoUri =
|
||||||
@ -213,34 +211,20 @@ class MessageContent extends StatelessWidget {
|
|||||||
textmessage:
|
textmessage:
|
||||||
default:
|
default:
|
||||||
if (event.content['msgtype'] == Matrix.callNamespace) {
|
if (event.content['msgtype'] == Matrix.callNamespace) {
|
||||||
return ElevatedButton.icon(
|
return _ButtonContent(
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
primary: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
onPrimary: Theme.of(context).textTheme.bodyText1.color,
|
|
||||||
),
|
|
||||||
onPressed: () => launch(event.body),
|
onPressed: () => launch(event.body),
|
||||||
icon: const Icon(Icons.phone_outlined, color: Colors.green),
|
icon: const Icon(Icons.phone_outlined, color: Colors.green),
|
||||||
label: Text(L10n.of(context).videoCall),
|
label: L10n.of(context).videoCall,
|
||||||
|
textColor: textColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (event.redacted) {
|
if (event.redacted) {
|
||||||
return Row(
|
return _ButtonContent(
|
||||||
mainAxisSize: MainAxisSize.min,
|
label: L10n.of(context)
|
||||||
children: [
|
.redactedAnEvent(event.sender.calcDisplayname()),
|
||||||
Icon(Icons.delete_forever_outlined, color: textColor),
|
icon: const Icon(Icons.delete_outlined),
|
||||||
const SizedBox(width: 4),
|
textColor: textColor,
|
||||||
Text(
|
onPressed: () => onInfoTab(event),
|
||||||
event.getLocalizedBody(MatrixLocals(L10n.of(context)),
|
|
||||||
hideReply: true),
|
|
||||||
style: TextStyle(
|
|
||||||
color: textColor,
|
|
||||||
fontSize: fontSize,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
decoration: TextDecoration.lineThrough,
|
|
||||||
decorationThickness: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final bigEmotes = event.onlyEmotes &&
|
final bigEmotes = event.onlyEmotes &&
|
||||||
@ -264,15 +248,42 @@ class MessageContent extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return Text(
|
return _ButtonContent(
|
||||||
L10n.of(context)
|
label: L10n.of(context)
|
||||||
.userSentUnknownEvent(event.sender.calcDisplayname(), event.type),
|
.userSentUnknownEvent(event.sender.calcDisplayname(), event.type),
|
||||||
style: TextStyle(
|
icon: const Icon(Icons.info_outlined),
|
||||||
color: textColor,
|
textColor: textColor,
|
||||||
decoration: event.redacted ? TextDecoration.lineThrough : null,
|
onPressed: () => onInfoTab(event),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Container(); // else flutter analyze complains
|
return Container(); // else flutter analyze complains
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ButtonContent extends StatelessWidget {
|
||||||
|
final void Function() onPressed;
|
||||||
|
final String label;
|
||||||
|
final Icon icon;
|
||||||
|
final Color textColor;
|
||||||
|
|
||||||
|
const _ButtonContent({
|
||||||
|
@required this.label,
|
||||||
|
@required this.icon,
|
||||||
|
@required this.textColor,
|
||||||
|
@required this.onPressed,
|
||||||
|
Key key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return OutlinedButton.icon(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
primary: textColor,
|
||||||
|
textStyle: TextStyle(color: textColor),
|
||||||
|
),
|
||||||
|
onPressed: onPressed,
|
||||||
|
icon: icon,
|
||||||
|
label: Text(label),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import 'package:characters/characters.dart';
|
|||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
@ -100,13 +101,12 @@ class _Reaction extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final borderColor = reacted
|
final borderColor = reacted
|
||||||
? Theme.of(context).primaryColor
|
? Theme.of(context).primaryColor
|
||||||
: Theme.of(context).secondaryHeaderColor;
|
: Theme.of(context).dividerColor;
|
||||||
final textColor = Theme.of(context).brightness == Brightness.dark
|
final textColor = Theme.of(context).brightness == Brightness.dark
|
||||||
? Colors.white
|
? Colors.white
|
||||||
: Colors.black;
|
: Colors.black;
|
||||||
final color = Theme.of(context).scaffoldBackgroundColor;
|
final color = Theme.of(context).scaffoldBackgroundColor;
|
||||||
final fontSize = DefaultTextStyle.of(context).style.fontSize;
|
final fontSize = DefaultTextStyle.of(context).style.fontSize;
|
||||||
final padding = fontSize / 5;
|
|
||||||
Widget content;
|
Widget content;
|
||||||
if (reactionKey.startsWith('mxc://')) {
|
if (reactionKey.startsWith('mxc://')) {
|
||||||
final src = Uri.parse(reactionKey)?.getThumbnail(
|
final src = Uri.parse(reactionKey)?.getThumbnail(
|
||||||
@ -122,7 +122,7 @@ class _Reaction extends StatelessWidget {
|
|||||||
imageUrl: src.toString(),
|
imageUrl: src.toString(),
|
||||||
height: fontSize,
|
height: fontSize,
|
||||||
),
|
),
|
||||||
Container(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(count.toString(),
|
Text(count.toString(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: textColor,
|
color: textColor,
|
||||||
@ -144,6 +144,7 @@ class _Reaction extends StatelessWidget {
|
|||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => onTap != null ? onTap() : null,
|
onTap: () => onTap != null ? onTap() : null,
|
||||||
onLongPress: () => onLongPress != null ? onLongPress() : null,
|
onLongPress: () => onLongPress != null ? onLongPress() : null,
|
||||||
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color,
|
color: color,
|
||||||
@ -151,9 +152,9 @@ class _Reaction extends StatelessWidget {
|
|||||||
width: 1,
|
width: 1,
|
||||||
color: borderColor,
|
color: borderColor,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.all(padding),
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
|
||||||
child: content,
|
child: content,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -22,8 +22,7 @@ class ReplyContent extends StatelessWidget {
|
|||||||
final displayEvent = replyEvent != null && timeline != null
|
final displayEvent = replyEvent != null && timeline != null
|
||||||
? replyEvent.getDisplayEvent(timeline)
|
? replyEvent.getDisplayEvent(timeline)
|
||||||
: replyEvent;
|
: replyEvent;
|
||||||
final fontSize =
|
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||||
DefaultTextStyle.of(context).style.fontSize * AppConfig.fontSizeFactor;
|
|
||||||
if (displayEvent != null &&
|
if (displayEvent != null &&
|
||||||
AppConfig.renderHtml &&
|
AppConfig.renderHtml &&
|
||||||
[EventTypes.Message, EventTypes.Encrypted]
|
[EventTypes.Message, EventTypes.Encrypted]
|
||||||
|
@ -31,11 +31,8 @@ class StateMessage extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
),
|
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -44,8 +41,7 @@ class StateMessage extends StatelessWidget {
|
|||||||
event.getLocalizedBody(MatrixLocals(L10n.of(context))),
|
event.getLocalizedBody(MatrixLocals(L10n.of(context))),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: Theme.of(context).textTheme.bodyText1.fontSize *
|
fontSize: Theme.of(context).textTheme.bodyText1.fontSize,
|
||||||
AppConfig.fontSizeFactor,
|
|
||||||
color: Theme.of(context).textTheme.bodyText2.color,
|
color: Theme.of(context).textTheme.bodyText2.color,
|
||||||
decoration:
|
decoration:
|
||||||
event.redacted ? TextDecoration.lineThrough : null,
|
event.redacted ? TextDecoration.lineThrough : null,
|
||||||
|
68
lib/pages/chat/seen_by_row.dart
Normal file
68
lib/pages/chat/seen_by_row.dart
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/pages/chat/chat.dart';
|
||||||
|
import 'package:fluffychat/utils/room_status_extension.dart';
|
||||||
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
|
||||||
|
class SeenByRow extends StatelessWidget {
|
||||||
|
final ChatController controller;
|
||||||
|
const SeenByRow(this.controller, {Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final seenByUsers = controller.room.getSeenByUsers(
|
||||||
|
controller.timeline,
|
||||||
|
controller.filteredEvents,
|
||||||
|
controller.unfolded,
|
||||||
|
);
|
||||||
|
const maxAvatars = 7;
|
||||||
|
return AnimatedContainer(
|
||||||
|
height: seenByUsers.isEmpty ? 0 : 24,
|
||||||
|
duration: seenByUsers.isEmpty
|
||||||
|
? const Duration(milliseconds: 0)
|
||||||
|
: const Duration(milliseconds: 300),
|
||||||
|
alignment: controller.filteredEvents.isNotEmpty &&
|
||||||
|
controller.filteredEvents.first.senderId ==
|
||||||
|
Matrix.of(context).client.userID
|
||||||
|
? Alignment.topRight
|
||||||
|
: Alignment.topLeft,
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: 4,
|
||||||
|
),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
...(seenByUsers.length > maxAvatars
|
||||||
|
? seenByUsers.sublist(0, maxAvatars)
|
||||||
|
: seenByUsers)
|
||||||
|
.map(
|
||||||
|
(user) => Avatar(
|
||||||
|
user.avatarUrl,
|
||||||
|
user.calcDisplayname(),
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
if (seenByUsers.length > maxAvatars)
|
||||||
|
SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: Material(
|
||||||
|
color: Theme.of(context).backgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'+${seenByUsers.length - maxAvatars}',
|
||||||
|
style: const TextStyle(fontSize: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,6 @@ import 'package:pedantic/pedantic.dart';
|
|||||||
import 'package:vrouter/vrouter.dart';
|
import 'package:vrouter/vrouter.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart';
|
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
||||||
import 'package:fluffychat/utils/room_status_extension.dart';
|
import 'package:fluffychat/utils/room_status_extension.dart';
|
||||||
import '../../utils/date_time_extension.dart';
|
import '../../utils/date_time_extension.dart';
|
||||||
@ -182,7 +181,7 @@ class ChatListItem extends StatelessWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: unread ? FontWeight.bold : null,
|
fontWeight: FontWeight.bold,
|
||||||
color: unread
|
color: unread
|
||||||
? Theme.of(context).colorScheme.secondary
|
? Theme.of(context).colorScheme.secondary
|
||||||
: Theme.of(context).textTheme.bodyText1.color,
|
: Theme.of(context).textTheme.bodyText1.color,
|
||||||
@ -224,13 +223,16 @@ class ChatListItem extends StatelessWidget {
|
|||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
if (typingText.isEmpty && ownMessage) ...{
|
if (typingText.isEmpty &&
|
||||||
Icon(
|
ownMessage &&
|
||||||
room.lastEvent.statusIcon,
|
room.lastEvent.status.isSending) ...[
|
||||||
size: 14,
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
},
|
],
|
||||||
AnimatedContainer(
|
AnimatedContainer(
|
||||||
width: typingText.isEmpty ? 0 : 18,
|
width: typingText.isEmpty ? 0 : 18,
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
@ -244,19 +246,6 @@ class ChatListItem extends StatelessWidget {
|
|||||||
size: 14,
|
size: 14,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (typingText.isEmpty &&
|
|
||||||
!ownMessage &&
|
|
||||||
!room.isDirectChat &&
|
|
||||||
room.lastEvent != null &&
|
|
||||||
room.lastEvent.type == EventTypes.Message &&
|
|
||||||
{MessageTypes.Text, MessageTypes.Notice}
|
|
||||||
.contains(room.lastEvent.messageType))
|
|
||||||
Text(
|
|
||||||
'${room.lastEvent.sender.calcDisplayname()}: ',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).textTheme.bodyText1.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: typingText.isNotEmpty
|
child: typingText.isNotEmpty
|
||||||
? Text(
|
? Text(
|
||||||
@ -274,6 +263,7 @@ class ChatListItem extends StatelessWidget {
|
|||||||
hideReply: true,
|
hideReply: true,
|
||||||
hideEdit: true,
|
hideEdit: true,
|
||||||
plaintextBody: true,
|
plaintextBody: true,
|
||||||
|
withSenderNamePrefix: true,
|
||||||
) ??
|
) ??
|
||||||
L10n.of(context).emptyChat,
|
L10n.of(context).emptyChat,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
|
@ -86,19 +86,20 @@ class SettingsStyleView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Container(
|
child: Material(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
color: Theme.of(context).backgroundColor,
|
||||||
padding:
|
elevation: 6,
|
||||||
const EdgeInsets.symmetric(vertical: 6, horizontal: 10),
|
shadowColor:
|
||||||
decoration: BoxDecoration(
|
Theme.of(context).secondaryHeaderColor.withAlpha(100),
|
||||||
color: Theme.of(context).secondaryHeaderColor,
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||||
borderRadius: BorderRadius.circular(16),
|
child: Padding(
|
||||||
),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor',
|
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: Theme.of(context).textTheme.bodyText1.fontSize *
|
fontSize:
|
||||||
AppConfig.fontSizeFactor,
|
AppConfig.messageFontSize * AppConfig.fontSizeFactor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -17,21 +17,6 @@ extension LocalizedBody on Event {
|
|||||||
matrixFile.result?.save(context);
|
matrixFile.result?.save(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
IconData get statusIcon {
|
|
||||||
switch (status.intValue) {
|
|
||||||
case -1:
|
|
||||||
return Icons.error_outline;
|
|
||||||
case 0:
|
|
||||||
return Icons.timer_outlined;
|
|
||||||
case 1:
|
|
||||||
return Icons.done_outlined;
|
|
||||||
case 2:
|
|
||||||
return Icons.done_all_outlined;
|
|
||||||
default:
|
|
||||||
return Icons.done_outlined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isAttachmentSmallEnough =>
|
bool get isAttachmentSmallEnough =>
|
||||||
infoMap['size'] is int &&
|
infoMap['size'] is int &&
|
||||||
infoMap['size'] < room.client.database.maxFileSize;
|
infoMap['size'] < room.client.database.maxFileSize;
|
||||||
|
@ -65,38 +65,26 @@ extension RoomStatusExtension on Room {
|
|||||||
return typingText;
|
return typingText;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getLocalizedSeenByText(
|
List<User> getSeenByUsers(
|
||||||
BuildContext context,
|
|
||||||
Timeline timeline,
|
Timeline timeline,
|
||||||
List<Event> filteredEvents,
|
List<Event> filteredEvents,
|
||||||
Set<String> unfolded,
|
Set<String> unfolded,
|
||||||
) {
|
) {
|
||||||
var seenByText = '';
|
if (timeline.events.isEmpty) return [];
|
||||||
if (timeline.events.isNotEmpty) {
|
|
||||||
final filteredEvents = timeline.getFilteredEvents(unfolded: unfolded);
|
final filteredEvents = timeline.getFilteredEvents(unfolded: unfolded);
|
||||||
if (filteredEvents.isEmpty) return '';
|
if (filteredEvents.isEmpty) return [];
|
||||||
final lastReceipts = <User>{};
|
|
||||||
// now we iterate the timeline events until we hit the first rendered event
|
final lastReceipts = <User>{};
|
||||||
for (final event in timeline.events) {
|
// now we iterate the timeline events until we hit the first rendered event
|
||||||
lastReceipts.addAll(event.receipts.map((r) => r.user));
|
for (final event in timeline.events) {
|
||||||
if (event.eventId == filteredEvents.first.eventId) {
|
lastReceipts.addAll(event.receipts.map((r) => r.user));
|
||||||
break;
|
if (event.eventId == filteredEvents.first.eventId) {
|
||||||
}
|
break;
|
||||||
}
|
|
||||||
lastReceipts.removeWhere((user) =>
|
|
||||||
user.id == client.userID || user.id == filteredEvents.first.senderId);
|
|
||||||
if (lastReceipts.length == 1) {
|
|
||||||
seenByText =
|
|
||||||
L10n.of(context).seenByUser(lastReceipts.first.calcDisplayname());
|
|
||||||
} else if (lastReceipts.length == 2) {
|
|
||||||
seenByText = seenByText = L10n.of(context).seenByUserAndUser(
|
|
||||||
lastReceipts.first.calcDisplayname(),
|
|
||||||
lastReceipts.last.calcDisplayname());
|
|
||||||
} else if (lastReceipts.length > 2) {
|
|
||||||
seenByText = L10n.of(context).seenByUserAndCountOthers(
|
|
||||||
lastReceipts.first.calcDisplayname(), lastReceipts.length - 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return seenByText;
|
lastReceipts.removeWhere((user) =>
|
||||||
|
user.id == client.userID || user.id == filteredEvents.first.senderId);
|
||||||
|
return lastReceipts.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user