Compare commits
1 commit
main
...
braid/scop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1a43dfb45 |
17 changed files with 1056 additions and 842 deletions
|
|
@ -1742,6 +1742,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"dynamicTheme": "Dynamisch",
|
||||
"theyDontMatch": "Stimmen nicht überein",
|
||||
"@theyDontMatch": {
|
||||
"type": "text",
|
||||
|
|
|
|||
|
|
@ -1936,6 +1936,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"dynamicTheme": "Dynamic",
|
||||
"theyDontMatch": "They Don't Match",
|
||||
"@theyDontMatch": {
|
||||
"type": "text",
|
||||
|
|
|
|||
|
|
@ -1759,6 +1759,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"dynamicTheme": "Dynamique",
|
||||
"theyDontMatch": "Elles ne correspondent pas",
|
||||
"@theyDontMatch": {
|
||||
"type": "text",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
|||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/app_lock.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/scoped_color_seed_builder.dart';
|
||||
import '../../utils/account_bundles.dart';
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
||||
|
|
@ -104,6 +105,8 @@ class ChatPageWithRoom extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ChatController extends State<ChatPageWithRoom> {
|
||||
final colorSeedController = ScopedColorSeedController();
|
||||
|
||||
Room get room => sendingClient.getRoomById(roomId) ?? widget.room;
|
||||
|
||||
late Client sendingClient;
|
||||
|
|
@ -1308,6 +1311,9 @@ class ChatController extends State<ChatPageWithRoom> {
|
|||
editEvent = null;
|
||||
});
|
||||
|
||||
void onProfileImageAvailable(Color value) =>
|
||||
colorSeedController.setSeed(value);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatView(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class ChatAppBarTitle extends StatelessWidget {
|
|||
),
|
||||
size: 32,
|
||||
presenceUserId: room.directChatMatrixID,
|
||||
onProfileColorCallback: controller.onProfileImageAvailable,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import 'package:fluffychat/pages/chat/tombstone_display.dart';
|
|||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
||||
import 'package:fluffychat/widgets/connection_status_header.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/scoped_color_seed_builder.dart';
|
||||
import 'package:fluffychat/widgets/unread_rooms_badge.dart';
|
||||
import '../../utils/stream_extension.dart';
|
||||
import 'chat_emoji_picker.dart';
|
||||
|
|
@ -136,238 +137,258 @@ class ChatView extends StatelessWidget {
|
|||
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
|
||||
final scrollUpBannerEventId = controller.scrollUpBannerEventId;
|
||||
|
||||
return PopScope(
|
||||
canPop: controller.selectedEvents.isEmpty && !controller.showEmojiPicker,
|
||||
onPopInvoked: (pop) async {
|
||||
if (pop) return;
|
||||
if (controller.selectedEvents.isNotEmpty) {
|
||||
controller.clearSelectedEvents();
|
||||
} else if (controller.showEmojiPicker) {
|
||||
controller.emojiPickerAction();
|
||||
}
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) => controller.setReadMarker(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: StreamBuilder(
|
||||
stream: controller.room.onUpdate.stream
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, snapshot) => FutureBuilder(
|
||||
future: controller.loadTimelineFuture,
|
||||
builder: (BuildContext context, snapshot) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actionsIconTheme: IconThemeData(
|
||||
color: controller.selectedEvents.isEmpty
|
||||
? null
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
leading: controller.selectMode
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: controller.clearSelectedEvents,
|
||||
tooltip: L10n.of(context)!.close,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: UnreadRoomsBadge(
|
||||
filter: (r) => r.id != controller.roomId,
|
||||
badgePosition: BadgePosition.topEnd(end: 8, top: 4),
|
||||
child: const Center(child: BackButton()),
|
||||
),
|
||||
titleSpacing: 0,
|
||||
title: ChatAppBarTitle(controller),
|
||||
actions: _appBarActions(context),
|
||||
),
|
||||
floatingActionButton: controller.showScrollDownButton &&
|
||||
controller.selectedEvents.isEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 56.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: controller.scrollDown,
|
||||
heroTag: null,
|
||||
mini: true,
|
||||
child: const Icon(Icons.arrow_downward_outlined),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
body: DropTarget(
|
||||
onDragDone: controller.onDragDone,
|
||||
onDragEntered: controller.onDragEntered,
|
||||
onDragExited: controller.onDragExited,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
if (Matrix.of(context).wallpaper != null)
|
||||
Image.file(
|
||||
Matrix.of(context).wallpaper!,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TombstoneDisplay(controller),
|
||||
if (scrollUpBannerEventId != null)
|
||||
Material(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant,
|
||||
shape: Border(
|
||||
bottom: BorderSide(
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: IconButton(
|
||||
return ScopedColorSeedBuilder(
|
||||
controller: controller.colorSeedController,
|
||||
builder: (context, color) {
|
||||
return PopScope(
|
||||
canPop:
|
||||
controller.selectedEvents.isEmpty && !controller.showEmojiPicker,
|
||||
onPopInvoked: (pop) async {
|
||||
if (pop) return;
|
||||
if (controller.selectedEvents.isNotEmpty) {
|
||||
controller.clearSelectedEvents();
|
||||
} else if (controller.showEmojiPicker) {
|
||||
controller.emojiPickerAction();
|
||||
}
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) => controller.setReadMarker(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: StreamBuilder(
|
||||
stream: controller.room.onUpdate.stream
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, snapshot) => FutureBuilder(
|
||||
future: controller.loadTimelineFuture,
|
||||
builder: (BuildContext context, snapshot) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
appBar: AppBar(
|
||||
actionsIconTheme: IconThemeData(
|
||||
color: controller.selectedEvents.isEmpty
|
||||
? null
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
leading: controller.selectMode
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: controller.clearSelectedEvents,
|
||||
tooltip: L10n.of(context)!.close,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: UnreadRoomsBadge(
|
||||
filter: (r) => r.id != controller.roomId,
|
||||
badgePosition:
|
||||
BadgePosition.topEnd(end: 8, top: 4),
|
||||
child: const Center(child: BackButton()),
|
||||
),
|
||||
titleSpacing: 0,
|
||||
title: ChatAppBarTitle(controller),
|
||||
actions: _appBarActions(context),
|
||||
),
|
||||
floatingActionButton: controller.showScrollDownButton &&
|
||||
controller.selectedEvents.isEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 56.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: controller.scrollDown,
|
||||
heroTag: null,
|
||||
mini: true,
|
||||
child: const Icon(Icons.arrow_downward_outlined),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
body: DropTarget(
|
||||
onDragDone: controller.onDragDone,
|
||||
onDragEntered: controller.onDragEntered,
|
||||
onDragExited: controller.onDragExited,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
if (Matrix.of(context).wallpaper != null)
|
||||
Image.file(
|
||||
Matrix.of(context).wallpaper!,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TombstoneDisplay(controller),
|
||||
if (scrollUpBannerEventId != null)
|
||||
Material(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: L10n.of(context)!.close,
|
||||
onPressed: () {
|
||||
controller.discardScrollUpBannerEventId();
|
||||
controller.setReadMarker();
|
||||
},
|
||||
.surfaceVariant,
|
||||
shape: Border(
|
||||
bottom: BorderSide(
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: IconButton(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: L10n.of(context)!.close,
|
||||
onPressed: () {
|
||||
controller
|
||||
.discardScrollUpBannerEventId();
|
||||
controller.setReadMarker();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
L10n.of(context)!.jumpToLastReadMessage,
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.only(left: 8),
|
||||
trailing: TextButton(
|
||||
onPressed: () {
|
||||
controller.scrollToEventId(
|
||||
scrollUpBannerEventId,
|
||||
);
|
||||
controller
|
||||
.discardScrollUpBannerEventId();
|
||||
},
|
||||
child: Text(L10n.of(context)!.jump),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
L10n.of(context)!.jumpToLastReadMessage,
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.only(left: 8),
|
||||
trailing: TextButton(
|
||||
onPressed: () {
|
||||
controller.scrollToEventId(
|
||||
scrollUpBannerEventId,
|
||||
);
|
||||
controller.discardScrollUpBannerEventId();
|
||||
},
|
||||
child: Text(L10n.of(context)!.jump),
|
||||
),
|
||||
),
|
||||
),
|
||||
PinnedEvents(controller),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: controller.clearSingleSelectedEvent,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (controller.timeline == null) {
|
||||
return const Center(
|
||||
child:
|
||||
CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
PinnedEvents(controller),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: controller.clearSingleSelectedEvent,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (controller.timeline == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator
|
||||
.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ChatEventList(
|
||||
controller: controller,
|
||||
);
|
||||
},
|
||||
return ChatEventList(
|
||||
controller: controller,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership ==
|
||||
Membership.join)
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: bottomSheetPadding,
|
||||
left: bottomSheetPadding,
|
||||
right: bottomSheetPadding,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.columnWidth * 2.5,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
bottomRight: Radius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withAlpha(64),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
child: controller
|
||||
.room.isAbandonedDMRoom ==
|
||||
true
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
foregroundColor:
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.archive_outlined,
|
||||
),
|
||||
onPressed:
|
||||
controller.leaveChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.leave,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.forum_outlined,
|
||||
),
|
||||
onPressed:
|
||||
controller.recreateChat,
|
||||
label: Text(
|
||||
L10n.of(context)!
|
||||
.reopenChat,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
ChatInputRow(controller),
|
||||
ChatEmojiPicker(controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (controller.dragging)
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.scaffoldBackgroundColor
|
||||
.withOpacity(0.9),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(
|
||||
Icons.upload_outlined,
|
||||
size: 100,
|
||||
),
|
||||
),
|
||||
if (controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership == Membership.join)
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: bottomSheetPadding,
|
||||
left: bottomSheetPadding,
|
||||
right: bottomSheetPadding,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.columnWidth * 2.5,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft:
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
bottomRight:
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withAlpha(64),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
child: controller.room.isAbandonedDMRoom ==
|
||||
true
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.all(16),
|
||||
foregroundColor:
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.archive_outlined,
|
||||
),
|
||||
onPressed: controller.leaveChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.leave,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.all(16),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.forum_outlined,
|
||||
),
|
||||
onPressed:
|
||||
controller.recreateChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.reopenChat,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
ChatInputRow(controller),
|
||||
ChatEmojiPicker(controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (controller.dragging)
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.scaffoldBackgroundColor
|
||||
.withOpacity(0.9),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(
|
||||
Icons.upload_outlined,
|
||||
size: 100,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
|||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/app_lock.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/scoped_color_seed_builder.dart';
|
||||
|
||||
enum AliasActions { copy, delete, setCanonical }
|
||||
|
||||
|
|
@ -35,6 +36,8 @@ class ChatDetails extends StatefulWidget {
|
|||
class ChatDetailsController extends State<ChatDetails> {
|
||||
bool displaySettings = false;
|
||||
|
||||
ScopedColorSeedController colorSeedController = ScopedColorSeedController();
|
||||
|
||||
void toggleDisplaySettings() =>
|
||||
setState(() => displaySettings = !displaySettings);
|
||||
|
||||
|
|
@ -397,6 +400,9 @@ class ChatDetailsController extends State<ChatDetails> {
|
|||
|
||||
static const fixedWidth = 360.0;
|
||||
|
||||
void onProfileImageAvailable(Color value) =>
|
||||
colorSeedController.setSeed(value);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatDetailsView(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:fluffychat/widgets/avatar.dart';
|
|||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/scoped_color_seed_builder.dart';
|
||||
import '../../utils/url_launcher.dart';
|
||||
|
||||
class ChatDetailsView extends StatelessWidget {
|
||||
|
|
@ -37,399 +38,429 @@ class ChatDetailsView extends StatelessWidget {
|
|||
|
||||
final isEmbedded = GoRouterState.of(context).fullPath == '/rooms/:roomid';
|
||||
|
||||
return StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
var members = room.getParticipants().toList()
|
||||
..sort((b, a) => a.powerLevel.compareTo(b.powerLevel));
|
||||
members = members.take(10).toList();
|
||||
final actualMembersCount = (room.summary.mInvitedMemberCount ?? 0) +
|
||||
(room.summary.mJoinedMemberCount ?? 0);
|
||||
final canRequestMoreMembers = members.length < actualMembersCount;
|
||||
final iconColor = Theme.of(context).textTheme.bodyLarge!.color;
|
||||
final displayname = room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: isEmbedded
|
||||
? null
|
||||
: AppBar(
|
||||
leading: const Center(child: BackButton()),
|
||||
elevation: Theme.of(context).appBarTheme.elevation,
|
||||
actions: <Widget>[
|
||||
if (room.canonicalAlias.isNotEmpty)
|
||||
IconButton(
|
||||
tooltip: L10n.of(context)!.share,
|
||||
icon: Icon(Icons.adaptive.share_outlined),
|
||||
onPressed: () => FluffyShare.share(
|
||||
AppConfig.inviteLinkPrefix + room.canonicalAlias,
|
||||
context,
|
||||
),
|
||||
),
|
||||
ChatSettingsPopupMenu(room, false),
|
||||
],
|
||||
title: Text(L10n.of(context)!.chatDetails),
|
||||
backgroundColor:
|
||||
Theme.of(context).appBarTheme.backgroundColor,
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: members.length + 1 + (canRequestMoreMembers ? 1 : 0),
|
||||
itemBuilder: (BuildContext context, int i) => i == 0
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Material(
|
||||
elevation: Theme.of(context)
|
||||
return ScopedColorSeedBuilder(
|
||||
controller: controller.colorSeedController,
|
||||
builder: (context, color) {
|
||||
return StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
var members = room.getParticipants().toList()
|
||||
..sort((b, a) => a.powerLevel.compareTo(b.powerLevel));
|
||||
members = members.take(10).toList();
|
||||
final actualMembersCount = (room.summary.mInvitedMemberCount ?? 0) +
|
||||
(room.summary.mJoinedMemberCount ?? 0);
|
||||
final canRequestMoreMembers = members.length < actualMembersCount;
|
||||
final iconColor = Theme.of(context).textTheme.bodyLarge!.color;
|
||||
final displayname = room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
);
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
appBar: isEmbedded
|
||||
? null
|
||||
: AppBar(
|
||||
leading: const Center(child: BackButton()),
|
||||
elevation: Theme.of(context).appBarTheme.elevation,
|
||||
actions: <Widget>[
|
||||
if (room.canonicalAlias.isNotEmpty)
|
||||
IconButton(
|
||||
tooltip: L10n.of(context)!.share,
|
||||
icon: Icon(Icons.adaptive.share_outlined),
|
||||
onPressed: () => FluffyShare.share(
|
||||
AppConfig.inviteLinkPrefix + room.canonicalAlias,
|
||||
context,
|
||||
),
|
||||
),
|
||||
ChatSettingsPopupMenu(room, false),
|
||||
],
|
||||
title: Text(L10n.of(context)!.chatDetails),
|
||||
backgroundColor:
|
||||
Theme.of(context).appBarTheme.backgroundColor,
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount:
|
||||
members.length + 1 + (canRequestMoreMembers ? 1 : 0),
|
||||
itemBuilder: (BuildContext context, int i) => i == 0
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Material(
|
||||
elevation: Theme.of(context)
|
||||
.appBarTheme
|
||||
.scrolledUnderElevation ??
|
||||
4,
|
||||
shadowColor: Theme.of(context)
|
||||
.appBarTheme
|
||||
.scrolledUnderElevation ??
|
||||
4,
|
||||
shadowColor: Theme.of(context)
|
||||
.appBarTheme
|
||||
.shadowColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
Avatar.defaultSize * 2.5,
|
||||
),
|
||||
),
|
||||
child: Hero(
|
||||
tag: isEmbedded
|
||||
? 'embedded_content_banner'
|
||||
: 'content_banner',
|
||||
child: Avatar(
|
||||
mxContent: room.avatar,
|
||||
name: displayname,
|
||||
size: Avatar.defaultSize * 2.5,
|
||||
fontSize: 18 * 2.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!room.isDirectChat &&
|
||||
room.canChangeStateEvent(
|
||||
EventTypes.RoomAvatar,
|
||||
))
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: FloatingActionButton.small(
|
||||
onPressed: controller.setAvatarAction,
|
||||
heroTag: null,
|
||||
child: const Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
.shadowColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color:
|
||||
Theme.of(context).dividerColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
Avatar.defaultSize * 2.5,
|
||||
),
|
||||
),
|
||||
child: Hero(
|
||||
tag: isEmbedded
|
||||
? 'embedded_content_banner'
|
||||
: 'content_banner',
|
||||
child: Avatar(
|
||||
mxContent: room.avatar,
|
||||
name: displayname,
|
||||
size: Avatar.defaultSize * 2.5,
|
||||
fontSize: 18 * 2.5,
|
||||
onProfileColorCallback: controller
|
||||
.onProfileImageAvailable,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () => room.isDirectChat
|
||||
? null
|
||||
: room.canChangeStateEvent(
|
||||
EventTypes.RoomName,
|
||||
)
|
||||
? controller.setDisplaynameAction()
|
||||
: FluffyShare.share(
|
||||
displayname,
|
||||
context,
|
||||
copyOnly: true,
|
||||
),
|
||||
icon: Icon(
|
||||
room.isDirectChat
|
||||
? Icons.chat_bubble_outline
|
||||
: room.canChangeStateEvent(
|
||||
EventTypes.RoomName,
|
||||
)
|
||||
? Icons.edit_outlined
|
||||
: Icons.copy_outlined,
|
||||
size: 16,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground,
|
||||
),
|
||||
label: Text(
|
||||
room.isDirectChat
|
||||
? L10n.of(context)!.directChat
|
||||
: displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => room.isDirectChat
|
||||
? null
|
||||
: context.push(
|
||||
'/rooms/${controller.roomId}/details/members',
|
||||
if (!room.isDirectChat &&
|
||||
room.canChangeStateEvent(
|
||||
EventTypes.RoomAvatar,
|
||||
))
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: FloatingActionButton.small(
|
||||
onPressed:
|
||||
controller.setAvatarAction,
|
||||
heroTag: null,
|
||||
child: const Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.group_outlined,
|
||||
size: 14,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary,
|
||||
),
|
||||
label: Text(
|
||||
L10n.of(context)!.countParticipants(
|
||||
actualMembersCount,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () => room.isDirectChat
|
||||
? null
|
||||
: room.canChangeStateEvent(
|
||||
EventTypes.RoomName,
|
||||
)
|
||||
? controller
|
||||
.setDisplaynameAction()
|
||||
: FluffyShare.share(
|
||||
displayname,
|
||||
context,
|
||||
copyOnly: true,
|
||||
),
|
||||
icon: Icon(
|
||||
room.isDirectChat
|
||||
? Icons.chat_bubble_outline
|
||||
: room.canChangeStateEvent(
|
||||
EventTypes.RoomName,
|
||||
)
|
||||
? Icons.edit_outlined
|
||||
: Icons.copy_outlined,
|
||||
size: 16,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground,
|
||||
),
|
||||
label: Text(
|
||||
room.isDirectChat
|
||||
? L10n.of(context)!.directChat
|
||||
: displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => room.isDirectChat
|
||||
? null
|
||||
: context.push(
|
||||
'/rooms/${controller.roomId}/details/members',
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.group_outlined,
|
||||
size: 14,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary,
|
||||
),
|
||||
label: Text(
|
||||
L10n.of(context)!.countParticipants(
|
||||
actualMembersCount,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
if (!room.canChangeStateEvent(EventTypes.RoomTopic))
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.chatDescription,
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: controller.setTopicAction,
|
||||
label: Text(
|
||||
L10n.of(context)!.setChatDescription,
|
||||
),
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: room.topic.isEmpty
|
||||
? L10n.of(context)!.noChatDescriptionYet
|
||||
: room.topic,
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle:
|
||||
const TextStyle(color: Colors.blueAccent),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontStyle: room.topic.isEmpty
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.color,
|
||||
decorationColor: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.color,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
if (room.joinRules == JoinRules.public)
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(Icons.link_outlined),
|
||||
),
|
||||
trailing:
|
||||
const Icon(Icons.chevron_right_outlined),
|
||||
onTap: controller.editAliases,
|
||||
title: Text(L10n.of(context)!.editRoomAliases),
|
||||
subtitle: Text(
|
||||
(room.canonicalAlias.isNotEmpty)
|
||||
? room.canonicalAlias
|
||||
: L10n.of(context)!.none,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(
|
||||
Icons.insert_emoticon_outlined,
|
||||
),
|
||||
),
|
||||
title: Text(L10n.of(context)!.emoteSettings),
|
||||
subtitle: Text(L10n.of(context)!.setCustomEmotes),
|
||||
onTap: controller.goToEmoteSettings,
|
||||
trailing:
|
||||
const Icon(Icons.chevron_right_outlined),
|
||||
),
|
||||
if (!room.isDirectChat)
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(Icons.shield_outlined),
|
||||
),
|
||||
title: Text(
|
||||
L10n.of(context)!.whoIsAllowedToJoinThisGroup,
|
||||
),
|
||||
trailing: room.canChangeJoinRules
|
||||
? const Icon(Icons.chevron_right_outlined)
|
||||
: null,
|
||||
subtitle: Text(
|
||||
room.joinRules?.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
) ??
|
||||
L10n.of(context)!.none,
|
||||
),
|
||||
onTap: room.canChangeJoinRules
|
||||
? controller.setJoinRules
|
||||
: null,
|
||||
),
|
||||
if (!room.isDirectChat)
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(Icons.visibility_outlined),
|
||||
),
|
||||
trailing: room.canChangeHistoryVisibility
|
||||
? const Icon(Icons.chevron_right_outlined)
|
||||
: null,
|
||||
title: Text(
|
||||
L10n.of(context)!.visibilityOfTheChatHistory,
|
||||
),
|
||||
subtitle: Text(
|
||||
room.historyVisibility?.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
) ??
|
||||
L10n.of(context)!.none,
|
||||
),
|
||||
onTap: room.canChangeHistoryVisibility
|
||||
? controller.setHistoryVisibility
|
||||
: null,
|
||||
),
|
||||
if (room.joinRules == JoinRules.public)
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(
|
||||
Icons.person_add_alt_1_outlined,
|
||||
),
|
||||
),
|
||||
trailing: room.canChangeGuestAccess
|
||||
? const Icon(Icons.chevron_right_outlined)
|
||||
: null,
|
||||
title: Text(
|
||||
L10n.of(context)!.areGuestsAllowedToJoin,
|
||||
),
|
||||
subtitle: Text(
|
||||
room.guestAccess.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
),
|
||||
),
|
||||
onTap: room.canChangeGuestAccess
|
||||
? controller.setGuestAccess
|
||||
: null,
|
||||
),
|
||||
if (!room.isDirectChat)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.chatPermissions),
|
||||
subtitle: Text(
|
||||
L10n.of(context)!.whoCanPerformWhichAction,
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(
|
||||
Icons.edit_attributes_outlined,
|
||||
),
|
||||
),
|
||||
trailing:
|
||||
const Icon(Icons.chevron_right_outlined),
|
||||
onTap: () => context.push(
|
||||
'/rooms/${room.id}/details/permissions',
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.countParticipants(
|
||||
actualMembersCount.toString(),
|
||||
),
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!room.isDirectChat && room.canInvite)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.inviteContact),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
radius: Avatar.defaultSize / 2,
|
||||
child: const Icon(Icons.add_outlined),
|
||||
),
|
||||
trailing:
|
||||
const Icon(Icons.chevron_right_outlined),
|
||||
onTap: () =>
|
||||
context.go('/rooms/${room.id}/invite'),
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
if (!room.canChangeStateEvent(EventTypes.RoomTopic))
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.chatDescription,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
)
|
||||
: i < members.length + 1
|
||||
? ParticipantListItem(members[i - 1])
|
||||
: ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.loadCountMoreParticipants(
|
||||
(actualMembersCount - members.length)
|
||||
.toString(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: controller.setTopicAction,
|
||||
label: Text(L10n.of(context)!.setChatDescription),
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: room.topic.isEmpty
|
||||
? L10n.of(context)!.noChatDescriptionYet
|
||||
: room.topic,
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle:
|
||||
const TextStyle(color: Colors.blueAccent),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontStyle: room.topic.isEmpty
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
color:
|
||||
Theme.of(context).textTheme.bodyMedium!.color,
|
||||
decorationColor:
|
||||
Theme.of(context).textTheme.bodyMedium!.color,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
if (room.joinRules == JoinRules.public)
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(Icons.link_outlined),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
onTap: controller.editAliases,
|
||||
title: Text(L10n.of(context)!.editRoomAliases),
|
||||
subtitle: Text(
|
||||
(room.canonicalAlias.isNotEmpty)
|
||||
? room.canonicalAlias
|
||||
: L10n.of(context)!.none,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(
|
||||
Icons.insert_emoticon_outlined,
|
||||
),
|
||||
),
|
||||
title: Text(L10n.of(context)!.emoteSettings),
|
||||
subtitle: Text(L10n.of(context)!.setCustomEmotes),
|
||||
onTap: controller.goToEmoteSettings,
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
),
|
||||
if (!room.isDirectChat)
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(Icons.shield_outlined),
|
||||
),
|
||||
title: Text(
|
||||
L10n.of(context)!.whoIsAllowedToJoinThisGroup,
|
||||
),
|
||||
trailing: room.canChangeJoinRules
|
||||
? const Icon(Icons.chevron_right_outlined)
|
||||
: null,
|
||||
subtitle: Text(
|
||||
room.joinRules?.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
) ??
|
||||
L10n.of(context)!.none,
|
||||
),
|
||||
onTap: room.canChangeJoinRules
|
||||
? controller.setJoinRules
|
||||
: null,
|
||||
),
|
||||
if (!room.isDirectChat)
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(Icons.visibility_outlined),
|
||||
),
|
||||
trailing: room.canChangeHistoryVisibility
|
||||
? const Icon(Icons.chevron_right_outlined)
|
||||
: null,
|
||||
title: Text(
|
||||
L10n.of(context)!.visibilityOfTheChatHistory,
|
||||
),
|
||||
subtitle: Text(
|
||||
room.historyVisibility?.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
) ??
|
||||
L10n.of(context)!.none,
|
||||
),
|
||||
onTap: room.canChangeHistoryVisibility
|
||||
? controller.setHistoryVisibility
|
||||
: null,
|
||||
),
|
||||
if (room.joinRules == JoinRules.public)
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(
|
||||
Icons.person_add_alt_1_outlined,
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
child: const Icon(
|
||||
Icons.group_outlined,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: room.canChangeGuestAccess
|
||||
? const Icon(Icons.chevron_right_outlined)
|
||||
: null,
|
||||
title: Text(
|
||||
L10n.of(context)!.areGuestsAllowedToJoin,
|
||||
),
|
||||
subtitle: Text(
|
||||
room.guestAccess.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
onTap: () => context.push(
|
||||
'/rooms/${controller.roomId!}/details/members',
|
||||
),
|
||||
trailing:
|
||||
const Icon(Icons.chevron_right_outlined),
|
||||
),
|
||||
onTap: room.canChangeGuestAccess
|
||||
? controller.setGuestAccess
|
||||
: null,
|
||||
),
|
||||
if (!room.isDirectChat)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.chatPermissions),
|
||||
subtitle: Text(
|
||||
L10n.of(context)!.whoCanPerformWhichAction,
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(
|
||||
Icons.edit_attributes_outlined,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
onTap: () => context
|
||||
.push('/rooms/${room.id}/details/permissions'),
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.countParticipants(
|
||||
actualMembersCount.toString(),
|
||||
),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!room.isDirectChat && room.canInvite)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.inviteContact),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
radius: Avatar.defaultSize / 2,
|
||||
child: const Icon(Icons.add_outlined),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
onTap: () => context.go('/rooms/${room.id}/invite'),
|
||||
),
|
||||
],
|
||||
)
|
||||
: i < members.length + 1
|
||||
? ParticipantListItem(members[i - 1])
|
||||
: ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.loadCountMoreParticipants(
|
||||
(actualMembersCount - members.length).toString(),
|
||||
),
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
child: const Icon(
|
||||
Icons.group_outlined,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
onTap: () => context.push(
|
||||
'/rooms/${controller.roomId!}/details/members',
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import 'package:fluffychat/utils/localized_exception_extension.dart';
|
|||
import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/theme_builder.dart';
|
||||
import '../../../utils/account_bundles.dart';
|
||||
import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
||||
import '../../utils/url_launcher.dart';
|
||||
|
|
@ -700,9 +701,6 @@ class ChatListController extends State<ChatList>
|
|||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatListView(this);
|
||||
|
||||
void _hackyWebRTCFixForWeb() {
|
||||
ChatList.contextForVoip = context;
|
||||
}
|
||||
|
|
@ -715,6 +713,12 @@ class ChatListController extends State<ChatList>
|
|||
|
||||
Future<void> dehydrate() =>
|
||||
SettingsSecurityController.dehydrateDevice(context);
|
||||
|
||||
void onProfileImageAvailable(Color color) =>
|
||||
ThemeController.of(context).profileThemeSeed = color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatListView(this);
|
||||
}
|
||||
|
||||
enum EditBundleAction { addToBundle, removeFromBundle }
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ class ClientChooserButton extends StatelessWidget {
|
|||
matrix.client.userID!.localpart,
|
||||
size: 32,
|
||||
fontSize: 12,
|
||||
onProfileColorCallback: controller.onProfileImageAvailable,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class SettingsStyleView extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
Text(
|
||||
L10n.of(context)!.systemTheme,
|
||||
L10n.of(context)!.dynamicTheme,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/widgets/permission_slider_dialog.dart';
|
||||
import 'package:fluffychat/widgets/scoped_color_seed_builder.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import 'user_bottom_sheet_view.dart';
|
||||
|
||||
|
|
@ -87,6 +88,8 @@ class UserBottomSheet extends StatefulWidget {
|
|||
}
|
||||
|
||||
class UserBottomSheetController extends State<UserBottomSheet> {
|
||||
ScopedColorSeedController colorSeedController = ScopedColorSeedController();
|
||||
|
||||
void participantAction(UserBottomSheetAction action) async {
|
||||
final user = widget.user;
|
||||
final userId = user?.id ?? widget.profile?.userId;
|
||||
|
|
@ -243,6 +246,9 @@ class UserBottomSheetController extends State<UserBottomSheet> {
|
|||
}
|
||||
}
|
||||
|
||||
void onProfileImageAvailable(Color value) =>
|
||||
colorSeedController.setSeed(value);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => UserBottomSheetView(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:fluffychat/utils/date_time_extension.dart';
|
|||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/presence_builder.dart';
|
||||
import 'package:fluffychat/widgets/scoped_color_seed_builder.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import 'user_bottom_sheet.dart';
|
||||
|
||||
|
|
@ -26,241 +27,251 @@ class UserBottomSheetView extends StatelessWidget {
|
|||
|
||||
final client = Matrix.of(controller.widget.outerContext).client;
|
||||
final profileSearchError = controller.widget.profileSearchError;
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: CloseButton(
|
||||
onPressed: Navigator.of(context, rootNavigator: false).pop,
|
||||
),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(displayname),
|
||||
PresenceBuilder(
|
||||
userId: userId,
|
||||
client: client,
|
||||
builder: (context, presence) {
|
||||
if (presence == null ||
|
||||
(presence.presence == PresenceType.offline &&
|
||||
presence.lastActiveTimestamp == null)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final dotColor = presence.presence.isOnline
|
||||
? Colors.green
|
||||
: presence.presence.isUnavailable
|
||||
? Colors.red
|
||||
: Colors.grey;
|
||||
return ScopedColorSeedBuilder(
|
||||
controller: controller.colorSeedController,
|
||||
builder: (context, color) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
appBar: AppBar(
|
||||
leading: CloseButton(
|
||||
onPressed: Navigator.of(context, rootNavigator: false).pop,
|
||||
),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(displayname),
|
||||
PresenceBuilder(
|
||||
userId: userId,
|
||||
client: client,
|
||||
builder: (context, presence) {
|
||||
if (presence == null ||
|
||||
(presence.presence == PresenceType.offline &&
|
||||
presence.lastActiveTimestamp == null)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final lastActiveTimestamp = presence.lastActiveTimestamp;
|
||||
final dotColor = presence.presence.isOnline
|
||||
? Colors.green
|
||||
: presence.presence.isUnavailable
|
||||
? Colors.red
|
||||
: Colors.grey;
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: dotColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
if (presence.currentlyActive == true)
|
||||
Text(
|
||||
L10n.of(context)!.currentlyActive,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
)
|
||||
else if (lastActiveTimestamp != null)
|
||||
Text(
|
||||
L10n.of(context)!.lastActiveAgo(
|
||||
lastActiveTimestamp.localizedTimeShort(context),
|
||||
final lastActiveTimestamp = presence.lastActiveTimestamp;
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: dotColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
if (presence.currentlyActive == true)
|
||||
Text(
|
||||
L10n.of(context)!.currentlyActive,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
)
|
||||
else if (lastActiveTimestamp != null)
|
||||
Text(
|
||||
L10n.of(context)!.lastActiveAgo(
|
||||
lastActiveTimestamp.localizedTimeShort(context),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
if (userId != client.userID &&
|
||||
!client.ignoredUsers.contains(userId))
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: OutlinedButton.icon(
|
||||
label: Text(
|
||||
L10n.of(context)!.ignore,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
actions: [
|
||||
if (userId != client.userID &&
|
||||
!client.ignoredUsers.contains(userId))
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: OutlinedButton.icon(
|
||||
label: Text(
|
||||
L10n.of(context)!.ignore,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.shield_outlined,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
onPressed: () => controller
|
||||
.participantAction(UserBottomSheetAction.ignore),
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.shield_outlined,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
onPressed: () => controller
|
||||
.participantAction(UserBottomSheetAction.ignore),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Material(
|
||||
elevation:
|
||||
Theme.of(context).appBarTheme.scrolledUnderElevation ??
|
||||
4,
|
||||
shadowColor: Theme.of(context).appBarTheme.shadowColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
Avatar.defaultSize * 2.5,
|
||||
),
|
||||
),
|
||||
child: Avatar(
|
||||
mxContent: avatarUrl,
|
||||
name: displayname,
|
||||
size: Avatar.defaultSize * 2.5,
|
||||
fontSize: 18 * 2.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () => FluffyShare.share(
|
||||
'https://matrix.to/#/$userId',
|
||||
context,
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.adaptive.share_outlined,
|
||||
size: 16,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
label: Text(
|
||||
displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => FluffyShare.share(
|
||||
userId,
|
||||
context,
|
||||
copyOnly: true,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.copy_outlined,
|
||||
size: 14,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
label: Text(
|
||||
userId,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (userId != client.userID)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
body: ListView(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Material(
|
||||
elevation: Theme.of(context)
|
||||
.appBarTheme
|
||||
.scrolledUnderElevation ??
|
||||
4,
|
||||
shadowColor: Theme.of(context).appBarTheme.shadowColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
Avatar.defaultSize * 2.5,
|
||||
),
|
||||
),
|
||||
child: Avatar(
|
||||
mxContent: avatarUrl,
|
||||
name: displayname,
|
||||
size: Avatar.defaultSize * 2.5,
|
||||
fontSize: 18 * 2.5,
|
||||
onProfileColorCallback:
|
||||
controller.onProfileImageAvailable,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () => FluffyShare.share(
|
||||
'https://matrix.to/#/$userId',
|
||||
context,
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.adaptive.share_outlined,
|
||||
size: 16,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
label: Text(
|
||||
displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => FluffyShare.share(
|
||||
userId,
|
||||
context,
|
||||
copyOnly: true,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.copy_outlined,
|
||||
size: 14,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
label: Text(
|
||||
userId,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => controller
|
||||
.participantAction(UserBottomSheetAction.message),
|
||||
icon: const Icon(Icons.forum_outlined),
|
||||
label: Text(L10n.of(context)!.sendAMessage),
|
||||
),
|
||||
),
|
||||
if (controller.widget.onMention != null)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.alternate_email_outlined),
|
||||
title: Text(L10n.of(context)!.mention),
|
||||
onTap: () =>
|
||||
controller.participantAction(UserBottomSheetAction.mention),
|
||||
),
|
||||
if (user != null && user.canChangePowerLevel)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.setPermissionsLevel),
|
||||
leading: const Icon(Icons.edit_attributes_outlined),
|
||||
onTap: () => controller
|
||||
.participantAction(UserBottomSheetAction.permission),
|
||||
),
|
||||
if (user != null && user.canKick)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.kickFromChat),
|
||||
leading: const Icon(Icons.exit_to_app_outlined),
|
||||
onTap: () =>
|
||||
controller.participantAction(UserBottomSheetAction.kick),
|
||||
),
|
||||
if (user != null &&
|
||||
user.canBan &&
|
||||
user.membership != Membership.ban)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.banFromChat),
|
||||
leading: const Icon(Icons.warning_sharp),
|
||||
onTap: () =>
|
||||
controller.participantAction(UserBottomSheetAction.ban),
|
||||
)
|
||||
else if (user != null &&
|
||||
user.canBan &&
|
||||
user.membership == Membership.ban)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.unbanFromChat),
|
||||
leading: const Icon(Icons.warning_outlined),
|
||||
onTap: () =>
|
||||
controller.participantAction(UserBottomSheetAction.unban),
|
||||
),
|
||||
if (user != null && user.id != client.userID)
|
||||
ListTile(
|
||||
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
iconColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
title: Text(L10n.of(context)!.reportUser),
|
||||
leading: const Icon(Icons.report_outlined),
|
||||
onTap: () =>
|
||||
controller.participantAction(UserBottomSheetAction.report),
|
||||
),
|
||||
if (profileSearchError != null)
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.warning_outlined,
|
||||
color: Colors.orange,
|
||||
),
|
||||
subtitle: Text(
|
||||
L10n.of(context)!.profileNotFound,
|
||||
style: const TextStyle(color: Colors.orange),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (userId != client.userID)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => controller
|
||||
.participantAction(UserBottomSheetAction.message),
|
||||
icon: const Icon(Icons.forum_outlined),
|
||||
label: Text(L10n.of(context)!.sendAMessage),
|
||||
),
|
||||
),
|
||||
if (controller.widget.onMention != null)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.alternate_email_outlined),
|
||||
title: Text(L10n.of(context)!.mention),
|
||||
onTap: () => controller
|
||||
.participantAction(UserBottomSheetAction.mention),
|
||||
),
|
||||
if (user != null && user.canChangePowerLevel)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.setPermissionsLevel),
|
||||
leading: const Icon(Icons.edit_attributes_outlined),
|
||||
onTap: () => controller
|
||||
.participantAction(UserBottomSheetAction.permission),
|
||||
),
|
||||
if (user != null && user.canKick)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.kickFromChat),
|
||||
leading: const Icon(Icons.exit_to_app_outlined),
|
||||
onTap: () => controller
|
||||
.participantAction(UserBottomSheetAction.kick),
|
||||
),
|
||||
if (user != null &&
|
||||
user.canBan &&
|
||||
user.membership != Membership.ban)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.banFromChat),
|
||||
leading: const Icon(Icons.warning_sharp),
|
||||
onTap: () =>
|
||||
controller.participantAction(UserBottomSheetAction.ban),
|
||||
)
|
||||
else if (user != null &&
|
||||
user.canBan &&
|
||||
user.membership == Membership.ban)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.unbanFromChat),
|
||||
leading: const Icon(Icons.warning_outlined),
|
||||
onTap: () => controller
|
||||
.participantAction(UserBottomSheetAction.unban),
|
||||
),
|
||||
if (user != null && user.id != client.userID)
|
||||
ListTile(
|
||||
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
iconColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
title: Text(L10n.of(context)!.reportUser),
|
||||
leading: const Icon(Icons.report_outlined),
|
||||
onTap: () => controller
|
||||
.participantAction(UserBottomSheetAction.report),
|
||||
),
|
||||
if (profileSearchError != null)
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.warning_outlined,
|
||||
color: Colors.orange,
|
||||
),
|
||||
subtitle: Text(
|
||||
L10n.of(context)!.profileNotFound,
|
||||
style: const TextStyle(color: Colors.orange),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -5,6 +7,7 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:fluffychat/utils/string_color.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
import 'package:fluffychat/widgets/presence_builder.dart';
|
||||
import 'package:fluffychat/widgets/scoped_color_seed_builder.dart';
|
||||
|
||||
class Avatar extends StatelessWidget {
|
||||
final Uri? mxContent;
|
||||
|
|
@ -16,6 +19,7 @@ class Avatar extends StatelessWidget {
|
|||
final double fontSize;
|
||||
final String? presenceUserId;
|
||||
final Color? presenceBackgroundColor;
|
||||
final ValueChanged<Color>? onProfileColorCallback;
|
||||
|
||||
const Avatar({
|
||||
this.mxContent,
|
||||
|
|
@ -26,9 +30,22 @@ class Avatar extends StatelessWidget {
|
|||
this.fontSize = 18,
|
||||
this.presenceUserId,
|
||||
this.presenceBackgroundColor,
|
||||
this.onProfileColorCallback,
|
||||
super.key,
|
||||
});
|
||||
|
||||
Future<void> _handleAvatarImageData(Uint8List data) async {
|
||||
final color = await ScopedColorSeedController.imageHelper(data);
|
||||
onProfileColorCallback?.call(color);
|
||||
}
|
||||
|
||||
void _handleNoAvatarImageColor(Color color) {
|
||||
final hsvColor = HSVColor.fromColor(color);
|
||||
// dim the color since [String.lightColorAvatar] is quite intense
|
||||
final dimmed = hsvColor.withSaturation(.25);
|
||||
onProfileColorCallback?.call(dimmed.toColor());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var fallbackLetters = '@';
|
||||
|
|
@ -56,6 +73,7 @@ class Avatar extends StatelessWidget {
|
|||
final presenceUserId = this.presenceUserId;
|
||||
final color =
|
||||
noPic ? name?.lightColorAvatar : Theme.of(context).secondaryHeaderColor;
|
||||
if (noPic && color != null) _handleNoAvatarImageColor(color);
|
||||
final container = Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
|
|
@ -74,6 +92,7 @@ class Avatar extends StatelessWidget {
|
|||
height: size,
|
||||
placeholder: (_) => textWidget,
|
||||
cacheKey: mxContent.toString(),
|
||||
onImageDataCallback: _handleAvatarImageData,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class MxcImage extends StatefulWidget {
|
|||
final ThumbnailMethod thumbnailMethod;
|
||||
final Widget Function(BuildContext context)? placeholder;
|
||||
final String? cacheKey;
|
||||
final ValueChanged<Uint8List>? onImageDataCallback;
|
||||
|
||||
const MxcImage({
|
||||
this.uri,
|
||||
|
|
@ -38,6 +39,7 @@ class MxcImage extends StatefulWidget {
|
|||
this.animationCurve = FluffyThemes.animationCurve,
|
||||
this.thumbnailMethod = ThumbnailMethod.scale,
|
||||
this.cacheKey,
|
||||
this.onImageDataCallback,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -48,6 +50,7 @@ class MxcImage extends StatefulWidget {
|
|||
class _MxcImageState extends State<MxcImage> {
|
||||
static final Map<String, Uint8List> _imageDataCache = {};
|
||||
Uint8List? _imageDataNoCache;
|
||||
|
||||
Uint8List? get _imageData {
|
||||
final cacheKey = widget.cacheKey;
|
||||
return cacheKey == null ? _imageDataNoCache : _imageDataCache[cacheKey];
|
||||
|
|
@ -90,6 +93,7 @@ class _MxcImageState extends State<MxcImage> {
|
|||
if (_isCached == null) {
|
||||
final cachedData = await client.database?.getFile(storeKey);
|
||||
if (cachedData != null) {
|
||||
widget.onImageDataCallback?.call(cachedData);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_imageData = cachedData;
|
||||
|
|
@ -108,7 +112,7 @@ class _MxcImageState extends State<MxcImage> {
|
|||
throw Exception();
|
||||
}
|
||||
final remoteData = response.bodyBytes;
|
||||
|
||||
widget.onImageDataCallback?.call(remoteData);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_imageData = remoteData;
|
||||
|
|
@ -122,8 +126,10 @@ class _MxcImageState extends State<MxcImage> {
|
|||
);
|
||||
if (data.detectFileType is MatrixImageFile) {
|
||||
if (!mounted) return;
|
||||
final bytes = data.bytes;
|
||||
widget.onImageDataCallback?.call(bytes);
|
||||
setState(() {
|
||||
_imageData = data.bytes;
|
||||
_imageData = bytes;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -131,7 +137,11 @@ class _MxcImageState extends State<MxcImage> {
|
|||
}
|
||||
|
||||
void _tryLoad(_) async {
|
||||
if (_imageData != null) return;
|
||||
final data = _imageData;
|
||||
if (data != null) {
|
||||
widget.onImageDataCallback?.call(data);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await _load();
|
||||
} catch (_) {
|
||||
|
|
|
|||
81
lib/widgets/scoped_color_seed_builder.dart
Normal file
81
lib/widgets/scoped_color_seed_builder.dart
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/widgets/theme_builder.dart';
|
||||
|
||||
typedef ColorSeedBuilder = Widget Function(BuildContext context, Color? color);
|
||||
|
||||
class ScopedColorSeedBuilder extends StatefulWidget {
|
||||
final ScopedColorSeedController controller;
|
||||
final ColorSeedBuilder builder;
|
||||
|
||||
const ScopedColorSeedBuilder({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ScopedColorSeedBuilder> createState() => _ScopedColorSeedBuilderState();
|
||||
}
|
||||
|
||||
class _ScopedColorSeedBuilderState extends State<ScopedColorSeedBuilder> {
|
||||
StreamSubscription<Color?>? _colorSchemeListener;
|
||||
Color? _color;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_colorSchemeListener =
|
||||
widget.controller._colorStreamController.stream.listen(_setColor);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _setColor(Color? seed) {
|
||||
if (seed != _color) setState(() => _color = seed);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fluffyThemeMode = ThemeController.of(context);
|
||||
|
||||
final color = _color;
|
||||
// if a custom primary color is defined or no custom seed set,
|
||||
// no need to adjust theme
|
||||
if (color == null || fluffyThemeMode.primaryColor != null) {
|
||||
return widget.builder.call(context, color);
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Theme(
|
||||
// build the proper FluffyChat theme with the given seed
|
||||
data: FluffyThemes.buildTheme(context, theme.brightness, color),
|
||||
child: Builder(
|
||||
builder: (context) => widget.builder.call(context, color),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_colorSchemeListener?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class ScopedColorSeedController {
|
||||
final _colorStreamController = StreamController<Color?>.broadcast();
|
||||
|
||||
void setSeed(Color? seed) => _colorStreamController.add(seed);
|
||||
|
||||
static Future<Color> imageHelper(Uint8List image) async {
|
||||
final scheme = await ColorScheme.fromImageProvider(
|
||||
provider: MemoryImage(image),
|
||||
);
|
||||
final color = scheme.primary;
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,14 @@ import 'package:dynamic_color/dynamic_color.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
typedef FluffyThemeBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
ThemeMode themeMode,
|
||||
Color?,
|
||||
);
|
||||
|
||||
class ThemeBuilder extends StatefulWidget {
|
||||
final Widget Function(
|
||||
BuildContext context,
|
||||
ThemeMode themeMode,
|
||||
Color? primaryColor,
|
||||
) builder;
|
||||
final FluffyThemeBuilder builder;
|
||||
|
||||
final String themeModeSettingsKey;
|
||||
final String primaryColorSettingsKey;
|
||||
|
|
@ -31,10 +33,21 @@ class ThemeController extends State<ThemeBuilder> {
|
|||
ThemeMode? _themeMode;
|
||||
Color? _primaryColor;
|
||||
|
||||
/// caching if ever set based on the profile pic
|
||||
Color? _profileThemeSeed;
|
||||
|
||||
ThemeMode get themeMode => _themeMode ?? ThemeMode.system;
|
||||
|
||||
Color? get primaryColor => _primaryColor;
|
||||
|
||||
/// Sets the primaryColor at runtime
|
||||
/// This won't store it but should rather be used for temporary theme changes
|
||||
/// E.g. used for the profile picture based theme
|
||||
///
|
||||
/// In case a custom theme is selected by the user, this call is ignored
|
||||
|
||||
set profileThemeSeed(Color? color) => _profileThemeSeed = color;
|
||||
|
||||
static ThemeController of(BuildContext context) =>
|
||||
Provider.of<ThemeController>(
|
||||
context,
|
||||
|
|
@ -51,6 +64,7 @@ class ThemeController extends State<ThemeBuilder> {
|
|||
setState(() {
|
||||
_themeMode = ThemeMode.values
|
||||
.singleWhereOrNull((value) => value.name == rawThemeMode);
|
||||
|
||||
_primaryColor = rawColor == null ? null : Color(rawColor);
|
||||
});
|
||||
}
|
||||
|
|
@ -94,7 +108,7 @@ class ThemeController extends State<ThemeBuilder> {
|
|||
builder: (light, _) => widget.builder(
|
||||
context,
|
||||
themeMode,
|
||||
primaryColor ?? light?.primary,
|
||||
primaryColor ?? _profileThemeSeed ?? light?.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue