feat: new design

This commit is contained in:
Krille Fear 2021-11-13 13:06:36 +01:00
parent 7e2148fa9b
commit e2cdad27e0
16 changed files with 626 additions and 513 deletions

View File

@ -2613,5 +2613,9 @@
"@zoomOut": { "@zoomOut": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
} },
"messageInfo": "Message info",
"time": "Time",
"messageType": "Message Type",
"sender": "Sender"
} }

View File

@ -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) {

View File

@ -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(

View File

@ -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;

View File

@ -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 {
), ),
), ),
), ),
], ),
), ],
), ),
], ],
), ),

View 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;
}
}

View File

@ -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,
),
],
);
}
}

View File

@ -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),
);
}
}

View File

@ -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,
), ),
); );

View File

@ -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]

View File

@ -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,

View 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),
),
),
),
),
],
),
);
}
}

View File

@ -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,

View File

@ -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,
),
), ),
), ),
), ),

View File

@ -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;

View File

@ -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();
} }
} }