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": {
"type": "text",
"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 get defaultHomeserver => _defaultHomeserver;
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 enableRegistration = true;
static const Color primaryColor = Color(0xFF5625BA);
@ -49,7 +50,7 @@ abstract class AppConfig {
static const String emojiFontName = 'Noto Emoji';
static const String emojiFontUrl =
'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 void loadFromJson(Map<String, dynamic> json) {

View File

@ -62,13 +62,6 @@ abstract class FluffyThemes {
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
),
),
popupMenuTheme: PopupMenuThemeData(
elevation: 4,
shape: RoundedRectangleBorder(
@ -83,6 +76,9 @@ abstract class FluffyThemes {
style: ElevatedButton.styleFrom(
primary: AppConfig.primaryColor,
onPrimary: Colors.white,
elevation: 6,
shadowColor: const Color(0x44000000),
minimumSize: const Size.fromHeight(48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
@ -90,7 +86,8 @@ abstract class FluffyThemes {
),
),
cardTheme: CardTheme(
elevation: 4,
elevation: 6,
shadowColor: const Color(0x44000000),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
@ -109,7 +106,8 @@ abstract class FluffyThemes {
fillColor: lighten(AppConfig.primaryColor, .51),
),
appBarTheme: const AppBarTheme(
elevation: 2,
elevation: 6,
shadowColor: Color(0x44000000),
systemOverlayStyle: SystemUiOverlayStyle.dark,
backgroundColor: Colors.white,
titleTextStyle: TextStyle(
@ -178,17 +176,11 @@ abstract class FluffyThemes {
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
primary: AppConfig.primaryColor,
onPrimary: Colors.white,
minimumSize: const Size.fromHeight(48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
@ -197,7 +189,7 @@ abstract class FluffyThemes {
),
snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating),
appBarTheme: const AppBarTheme(
elevation: 2,
elevation: 6,
systemOverlayStyle: SystemUiOverlayStyle.light,
backgroundColor: Color(0xff1D1D1D),
titleTextStyle: TextStyle(

View File

@ -21,6 +21,7 @@ import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.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/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
@ -790,6 +791,9 @@ class ChatController extends State<Chat> {
setState(() => inputText = text);
}
void showEventInfo([Event event]) =>
(event ?? selectedEvents.single).showInfoDialog(context);
void cancelReplyEventAction() => setState(() {
if (editEvent != null) {
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/reactions_picker.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/user_bottom_sheet/user_bottom_sheet.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 'events/message.dart';
enum _EventContextAction { info, report }
class ChatView extends StatelessWidget {
final ChatController controller;
@ -56,6 +59,7 @@ class ChatView extends StatelessWidget {
showFutureLoadingDialog(
context: context, future: () => controller.room.join());
}
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
return VWidgetGuard(
onSystemPop: (redirector) async {
@ -159,18 +163,50 @@ class ChatView extends StatelessWidget {
tooltip: L10n.of(context).copy,
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)
IconButton(
icon: const Icon(Icons.delete_outlined),
tooltip: L10n.of(context).redactMessage,
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>[
if (controller.room.canSendDefaultStates)
@ -197,6 +233,8 @@ class ChatView extends StatelessWidget {
),
)
: null,
backgroundColor: Theme.of(context).primaryColor.withAlpha(
Theme.of(context).brightness == Brightness.light ? 15 : 70),
body: Stack(
children: <Widget>[
if (Matrix.of(context).wallpaper != null)
@ -206,199 +244,155 @@ class ChatView extends StatelessWidget {
height: double.infinity,
fit: BoxFit.cover,
),
SafeArea(
child: Column(
children: <Widget>[
TombstoneDisplay(controller),
Expanded(
child: FutureBuilder<bool>(
future: controller.getTimeline(),
builder: (BuildContext context, snapshot) {
if (controller.timeline == null) {
return const Center(
child: CircularProgressIndicator.adaptive(
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,
Column(
children: <Widget>[
TombstoneDisplay(controller),
Expanded(
child: FutureBuilder<bool>(
future: controller.getTimeline(),
builder: (BuildContext context, snapshot) {
if (controller.timeline == null) {
return const Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2),
);
return ListView.custom(
padding: const EdgeInsets.only(
top: 16,
bottom: 4,
),
reverse: true,
controller: controller.scrollController,
keyboardDismissBehavior: PlatformInfos.isIOS
? ScrollViewKeyboardDismissBehavior.onDrag
: ScrollViewKeyboardDismissBehavior.manual,
childrenDelegate: SliverChildBuilderDelegate(
(BuildContext context, int i) {
return i == controller.filteredEvents.length + 1
? controller.timeline.isRequestingHistory
? const Center(
child: CircularProgressIndicator
.adaptive(strokeWidth: 2),
)
: controller.canLoadMore
? Center(
child: OutlinedButton(
style:
OutlinedButton.styleFrom(
backgroundColor: Theme.of(
context)
.scaffoldBackgroundColor,
),
onPressed:
controller.requestHistory,
child: Text(L10n.of(context)
.loadMore),
}
// 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;
}
return ListView.custom(
padding: const EdgeInsets.only(
top: 16,
bottom: 4,
),
reverse: true,
controller: controller.scrollController,
keyboardDismissBehavior: PlatformInfos.isIOS
? ScrollViewKeyboardDismissBehavior.onDrag
: ScrollViewKeyboardDismissBehavior.manual,
childrenDelegate: SliverChildBuilderDelegate(
(BuildContext context, int i) {
return i == controller.filteredEvents.length + 1
? controller.timeline.isRequestingHistory
? const Center(
child: CircularProgressIndicator
.adaptive(strokeWidth: 2),
)
: controller.canLoadMore
? Center(
child: OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: Theme.of(
context)
.scaffoldBackgroundColor,
),
)
: Container()
: i == 0
? AnimatedContainer(
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),
onPressed:
controller.requestHistory,
child: Text(
L10n.of(context).loadMore),
),
child: Text(
seenByText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.secondary,
),
),
),
)
: AutoScrollTag(
)
: Container()
: i == 0
? SeenByRow(controller)
: AutoScrollTag(
key: ValueKey(controller
.filteredEvents[i - 1].eventId),
index: i - 1,
controller:
controller.scrollController,
child: Swipeable(
key: ValueKey(controller
.filteredEvents[i - 1].eventId),
index: i - 1,
controller:
controller.scrollController,
child: Swipeable(
key: ValueKey(controller
.filteredEvents[i - 1]
.eventId),
background: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 12.0),
child: Center(
child: Icon(
Icons.reply_outlined),
),
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),
),
);
},
childCount: controller.filteredEvents.length + 2,
findChildIndexCallback: (key) =>
controller.findChildIndexCallback(
key, thisEventsKeyMap),
),
);
},
),
direction:
SwipeDirection.endToStart,
onSwipe: (direction) =>
controller.replyAction(
replyTo: controller
.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(
height: 1,
),
if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
Padding(
padding: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
Padding(
padding: EdgeInsets.all(
FluffyThemes.isColumnMode(context) ? 16.0 : 8.0),
child: Material(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
elevation: 7,
clipBehavior: Clip.hardEdge,
color: Theme.of(context).appBarTheme.backgroundColor,
child: Material(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(AppConfig.borderRadius),
bottomRight: Radius.circular(AppConfig.borderRadius),
),
elevation: 6,
shadowColor: Theme.of(context)
.secondaryHeaderColor
.withAlpha(100),
clipBehavior: Clip.hardEdge,
color: Theme.of(context).backgroundColor,
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
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/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/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -20,6 +19,7 @@ class Message extends StatelessWidget {
final Event nextEvent;
final void Function(Event) onSelect;
final void Function(Event) onAvatarTab;
final void Function(Event) onInfoTab;
final void Function(String) scrollToEventId;
final void Function(String) unfold;
final bool longPressSelect;
@ -30,6 +30,7 @@ class Message extends StatelessWidget {
{this.nextEvent,
this.longPressSelect,
this.onSelect,
this.onInfoTab,
this.onAvatarTab,
this.scrollToEventId,
@required this.unfold,
@ -57,12 +58,19 @@ class Message extends StatelessWidget {
final client = Matrix.of(context).client;
final ownMessage = event.senderId == client.userID;
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 &&
[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;
var textColor = ownMessage
final textColor = ownMessage
? Colors.white
: Theme.of(context).brightness == Brightness.dark
? Colors.white
@ -71,148 +79,182 @@ class Message extends StatelessWidget {
ownMessage ? MainAxisAlignment.end : MainAxisAlignment.start;
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) {
color = Colors.transparent;
textColor = Theme.of(context).textTheme.bodyText2.color;
} else if (ownMessage) {
if (ownMessage) {
color = displayEvent.status.isError
? Colors.redAccent
: Theme.of(context).primaryColor;
}
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(
child: Container(
alignment: alignment,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Material(
color: color,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: InkWell(
onHover: (b) => useMouse = true,
onTap: !useMouse && longPressSelect
? () => null
: () => onSelect(event),
onLongPress: !longPressSelect ? null : () => onSelect(event),
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Container(
decoration: BoxDecoration(
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,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (!ownMessage && !sameSender)
Padding(
padding: const EdgeInsets.only(left: 8.0, bottom: 4),
child: event.room.isDirectChat
? const SizedBox(height: 12)
: Text(
event.sender.calcDisplayname(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: event.sender.calcDisplayname().color,
),
const SizedBox(height: 3),
Opacity(
opacity: 0,
child: _MetaRow(
event, // meta information should be from the unedited event
ownMessage,
textColor,
timeline,
displayEvent,
),
),
),
Container(
alignment: alignment,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Material(
color: color,
elevation: 6,
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(
crossAxisAlignment: CrossAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: rowMainAxisAlignment,
children: rowChildren,
);
Widget container;
if (event.hasAggregatedEvents(timeline, RelationshipTypes.reaction)) {
if (event.hasAggregatedEvents(timeline, RelationshipTypes.reaction) ||
displayTime) {
container = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
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,
Padding(
padding: EdgeInsets.only(
top: 4.0,
left: (ownMessage ? 0 : Avatar.defaultSize) + 12.0,
right: (ownMessage ? Avatar.defaultSize : 0) + 12.0,
right: 12.0,
),
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 {
final Event event;
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 {
if (event.content['can_request_session'] != true) {
@ -72,8 +74,7 @@ class MessageContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
final fontSize =
DefaultTextStyle.of(context).style.fontSize * AppConfig.fontSizeFactor;
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
switch (event.type) {
case EventTypes.Message:
case EventTypes.Encrypted:
@ -163,14 +164,11 @@ class MessageContent extends StatelessWidget {
continue textmessage;
case MessageTypes.BadEncrypted:
case EventTypes.Encrypted:
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).scaffoldBackgroundColor,
onPrimary: Theme.of(context).textTheme.bodyText1.color,
),
return _ButtonContent(
textColor: textColor,
onPressed: () => _verifyOrRequestKey(context),
icon: const Icon(Icons.lock_outline),
label: Text(L10n.of(context).encrypted),
label: L10n.of(context).encrypted,
);
case MessageTypes.Location:
final geoUri =
@ -213,34 +211,20 @@ class MessageContent extends StatelessWidget {
textmessage:
default:
if (event.content['msgtype'] == Matrix.callNamespace) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).scaffoldBackgroundColor,
onPrimary: Theme.of(context).textTheme.bodyText1.color,
),
return _ButtonContent(
onPressed: () => launch(event.body),
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) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.delete_forever_outlined, color: textColor),
const SizedBox(width: 4),
Text(
event.getLocalizedBody(MatrixLocals(L10n.of(context)),
hideReply: true),
style: TextStyle(
color: textColor,
fontSize: fontSize,
fontWeight: FontWeight.bold,
decoration: TextDecoration.lineThrough,
decorationThickness: 0.5,
),
),
],
return _ButtonContent(
label: L10n.of(context)
.redactedAnEvent(event.sender.calcDisplayname()),
icon: const Icon(Icons.delete_outlined),
textColor: textColor,
onPressed: () => onInfoTab(event),
);
}
final bigEmotes = event.onlyEmotes &&
@ -264,15 +248,42 @@ class MessageContent extends StatelessWidget {
}
break;
default:
return Text(
L10n.of(context)
return _ButtonContent(
label: L10n.of(context)
.userSentUnknownEvent(event.sender.calcDisplayname(), event.type),
style: TextStyle(
color: textColor,
decoration: event.redacted ? TextDecoration.lineThrough : null,
),
icon: const Icon(Icons.info_outlined),
textColor: textColor,
onPressed: () => onInfoTab(event),
);
}
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:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -100,13 +101,12 @@ class _Reaction extends StatelessWidget {
Widget build(BuildContext context) {
final borderColor = reacted
? Theme.of(context).primaryColor
: Theme.of(context).secondaryHeaderColor;
: Theme.of(context).dividerColor;
final textColor = Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black;
final color = Theme.of(context).scaffoldBackgroundColor;
final fontSize = DefaultTextStyle.of(context).style.fontSize;
final padding = fontSize / 5;
Widget content;
if (reactionKey.startsWith('mxc://')) {
final src = Uri.parse(reactionKey)?.getThumbnail(
@ -122,7 +122,7 @@ class _Reaction extends StatelessWidget {
imageUrl: src.toString(),
height: fontSize,
),
Container(width: 4),
const SizedBox(width: 4),
Text(count.toString(),
style: TextStyle(
color: textColor,
@ -144,6 +144,7 @@ class _Reaction extends StatelessWidget {
return InkWell(
onTap: () => onTap != null ? onTap() : null,
onLongPress: () => onLongPress != null ? onLongPress() : null,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Container(
decoration: BoxDecoration(
color: color,
@ -151,9 +152,9 @@ class _Reaction extends StatelessWidget {
width: 1,
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,
),
);

View File

@ -22,8 +22,7 @@ class ReplyContent extends StatelessWidget {
final displayEvent = replyEvent != null && timeline != null
? replyEvent.getDisplayEvent(timeline)
: replyEvent;
final fontSize =
DefaultTextStyle.of(context).style.fontSize * AppConfig.fontSizeFactor;
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
if (displayEvent != null &&
AppConfig.renderHtml &&
[EventTypes.Message, EventTypes.Encrypted]

View File

@ -31,11 +31,8 @@ class StateMessage extends StatelessWidget {
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
),
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
),
child: Column(
mainAxisSize: MainAxisSize.min,
@ -44,8 +41,7 @@ class StateMessage extends StatelessWidget {
event.getLocalizedBody(MatrixLocals(L10n.of(context))),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: Theme.of(context).textTheme.bodyText1.fontSize *
AppConfig.fontSizeFactor,
fontSize: Theme.of(context).textTheme.bodyText1.fontSize,
color: Theme.of(context).textTheme.bodyText2.color,
decoration:
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: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/room_status_extension.dart';
import '../../utils/date_time_extension.dart';
@ -182,7 +181,7 @@ class ChatListItem extends StatelessWidget {
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle(
fontWeight: unread ? FontWeight.bold : null,
fontWeight: FontWeight.bold,
color: unread
? Theme.of(context).colorScheme.secondary
: Theme.of(context).textTheme.bodyText1.color,
@ -224,13 +223,16 @@ class ChatListItem extends StatelessWidget {
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (typingText.isEmpty && ownMessage) ...{
Icon(
room.lastEvent.statusIcon,
size: 14,
if (typingText.isEmpty &&
ownMessage &&
room.lastEvent.status.isSending) ...[
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
),
const SizedBox(width: 4),
},
],
AnimatedContainer(
width: typingText.isEmpty ? 0 : 18,
clipBehavior: Clip.hardEdge,
@ -244,19 +246,6 @@ class ChatListItem extends StatelessWidget {
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(
child: typingText.isNotEmpty
? Text(
@ -274,6 +263,7 @@ class ChatListItem extends StatelessWidget {
hideReply: true,
hideEdit: true,
plaintextBody: true,
withSenderNamePrefix: true,
) ??
L10n.of(context).emptyChat,
softWrap: false,

View File

@ -86,19 +86,20 @@ class SettingsStyleView extends StatelessWidget {
),
Container(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding:
const EdgeInsets.symmetric(vertical: 6, horizontal: 10),
decoration: BoxDecoration(
color: Theme.of(context).secondaryHeaderColor,
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor',
style: TextStyle(
fontSize: Theme.of(context).textTheme.bodyText1.fontSize *
AppConfig.fontSizeFactor,
child: Material(
color: Theme.of(context).backgroundColor,
elevation: 6,
shadowColor:
Theme.of(context).secondaryHeaderColor.withAlpha(100),
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor',
style: TextStyle(
fontSize:
AppConfig.messageFontSize * AppConfig.fontSizeFactor,
),
),
),
),

View File

@ -17,21 +17,6 @@ extension LocalizedBody on Event {
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 =>
infoMap['size'] is int &&
infoMap['size'] < room.client.database.maxFileSize;

View File

@ -65,38 +65,26 @@ extension RoomStatusExtension on Room {
return typingText;
}
String getLocalizedSeenByText(
BuildContext context,
List<User> getSeenByUsers(
Timeline timeline,
List<Event> filteredEvents,
Set<String> unfolded,
) {
var seenByText = '';
if (timeline.events.isNotEmpty) {
final filteredEvents = timeline.getFilteredEvents(unfolded: unfolded);
if (filteredEvents.isEmpty) return '';
final lastReceipts = <User>{};
// now we iterate the timeline events until we hit the first rendered event
for (final event in timeline.events) {
lastReceipts.addAll(event.receipts.map((r) => r.user));
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);
if (timeline.events.isEmpty) return [];
final filteredEvents = timeline.getFilteredEvents(unfolded: unfolded);
if (filteredEvents.isEmpty) return [];
final lastReceipts = <User>{};
// now we iterate the timeline events until we hit the first rendered event
for (final event in timeline.events) {
lastReceipts.addAll(event.receipts.map((r) => r.user));
if (event.eventId == filteredEvents.first.eventId) {
break;
}
}
return seenByText;
lastReceipts.removeWhere((user) =>
user.id == client.userID || user.id == filteredEvents.first.senderId);
return lastReceipts.toList();
}
}