mirror of
https://github.com/krille-chan/fluffychat
synced 2024-09-10 11:45:10 +00:00
feat: new design
This commit is contained in:
parent
7355d6609d
commit
22fdb667ba
16 changed files with 626 additions and 513 deletions
|
@ -2613,5 +2613,9 @@
|
|||
"@zoomOut": {
|
||||
"type": "text",
|
||||
"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 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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
|||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
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/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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
|
|
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: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,
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue