2
0
mirror of https://gitlab.com/famedly/fluffychat.git synced 2025-07-05 02:37:23 +02:00

refactor: Folder structure and MVC chat ui

This commit is contained in:
Christian Pauly 2021-04-15 13:03:14 +02:00
parent 1fe5b78ec6
commit fb618243f5
47 changed files with 1653 additions and 1587 deletions

@ -2,14 +2,37 @@
FluffyChat tries to be as minimal as possible even in the code style. We try to keep the code clean, simple and easy to read. The source code of the app is under `/lib` with the main entry point `/lib/main.dart`. FluffyChat tries to be as minimal as possible even in the code style. We try to keep the code clean, simple and easy to read. The source code of the app is under `/lib` with the main entry point `/lib/main.dart`.
### Directory Structure ### Directory Structure:
- /lib
- /config
- app_config.dart
- ...Constants, styles and other configurations
- /l10n
- intl_en.arb
- ...Localization files
- /models
- app_model.dart
- ...Data models used in the app
- /utils
- handy_function.dart
- ...Helper functions and extensions
- /views
- /ui
- home_ui.dart
- details_ui.dart
- /widgets
- /dialogs
- /ui
- /list_items
- /ui
- /ui
- home_view.dart
- details_view.dart
- ...The views and widgets of the app separated in Controllers and Views
- main.dart
- `/lib/config/` Constants, styles and other configurations
- `/lib/controllers/` Controller classes regarding the MVC separation
- `/lib/l10n/` Localization files wi
- `/lib/utils/` Helper functions and extensions
- `/lib/views/` View classes and widgets
- `/lib/views/widgets/` Reusable Flutter widgets
Most of the business model is in the Famedly Matrix Dart SDK. We try to not keep a model inside of the source code but extend it under `/utils`. Most of the business model is in the Famedly Matrix Dart SDK. We try to not keep a model inside of the source code but extend it under `/utils`.

@ -1,31 +1,31 @@
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/controllers/archive_controller.dart'; import 'package:fluffychat/views/archive.dart';
import 'package:fluffychat/controllers/homeserver_picker_controller.dart'; import 'package:fluffychat/views/homeserver_picker.dart';
import 'package:fluffychat/controllers/invitation_selection_controller.dart'; import 'package:fluffychat/views/invitation_selection.dart';
import 'package:fluffychat/controllers/sign_up_controller.dart'; import 'package:fluffychat/views/sign_up.dart';
import 'package:fluffychat/controllers/sign_up_password_controller.dart'; import 'package:fluffychat/views/sign_up_password.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:fluffychat/views/chat.dart'; import 'package:fluffychat/views/chat.dart';
import 'package:fluffychat/controllers/chat_details_controller.dart'; import 'package:fluffychat/views/chat_details.dart';
import 'package:fluffychat/controllers/chat_encryption_settings_controller.dart'; import 'package:fluffychat/views/chat_encryption_settings.dart';
import 'package:fluffychat/controllers/chat_list_controller.dart'; import 'package:fluffychat/views/chat_list.dart';
import 'package:fluffychat/controllers/chat_permissions_settings_controller.dart'; import 'package:fluffychat/views/chat_permissions_settings.dart';
import 'package:fluffychat/views/empty_page.dart'; import 'package:fluffychat/views/ui/empty_page_ui.dart';
import 'package:fluffychat/views/widgets/loading_view.dart'; import 'package:fluffychat/views/widgets/loading_view.dart';
import 'package:fluffychat/views/widgets/log_view.dart'; import 'package:fluffychat/views/widgets/log_view.dart';
import 'package:fluffychat/views/login.dart'; import 'package:fluffychat/views/ui/login_ui.dart';
import 'package:fluffychat/controllers/new_group_controller.dart'; import 'package:fluffychat/views/new_group.dart';
import 'package:fluffychat/controllers/new_private_chat_controller.dart'; import 'package:fluffychat/views/new_private_chat.dart';
import 'package:fluffychat/views/search_view.dart'; import 'package:fluffychat/views/ui/search_ui.dart';
import 'package:fluffychat/views/settings.dart'; import 'package:fluffychat/views/ui/settings_ui.dart';
import 'package:fluffychat/views/settings_3pid.dart'; import 'package:fluffychat/views/ui/settings_3pid_ui.dart';
import 'package:fluffychat/controllers/device_settings_controller.dart'; import 'package:fluffychat/views/device_settings.dart';
import 'package:fluffychat/views/settings_emotes.dart'; import 'package:fluffychat/views/ui/settings_emotes_ui.dart';
import 'package:fluffychat/views/settings_ignore_list.dart'; import 'package:fluffychat/views/ui/settings_ignore_list_ui.dart';
import 'package:fluffychat/views/settings_multiple_emotes.dart'; import 'package:fluffychat/views/ui/settings_multiple_emotes_ui.dart';
import 'package:fluffychat/views/settings_notifications.dart'; import 'package:fluffychat/views/ui/settings_notifications_ui.dart';
import 'package:fluffychat/views/settings_style.dart'; import 'package:fluffychat/views/ui/settings_style_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class FluffyRoutes { class FluffyRoutes {

@ -1,223 +0,0 @@
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:file_picker_cross/file_picker_cross.dart';
import 'package:fluffychat/views/chat_details.dart';
import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/utils/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:image_picker/image_picker.dart';
class ChatDetails extends StatefulWidget {
final String roomId;
const ChatDetails(this.roomId);
@override
ChatDetailsController createState() => ChatDetailsController();
}
class ChatDetailsController extends State<ChatDetails> {
List<User> members;
@override
void initState() {
super.initState();
members ??=
Matrix.of(context).client.getRoomById(widget.roomId).getParticipants();
}
void setDisplaynameAction() async {
final room = Matrix.of(context).client.getRoomById(widget.roomId);
final input = await showTextInputDialog(
context: context,
title: L10n.of(context).changeTheNameOfTheGroup,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
useRootNavigator: false,
textFields: [
DialogTextField(
initialText: room.getLocalizedDisplayname(
MatrixLocals(
L10n.of(context),
),
),
)
],
);
if (input == null) return;
final success = await showFutureLoadingDialog(
context: context,
future: () => room.setName(input.single),
);
if (success.error == null) {
AdaptivePageLayout.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).displaynameHasBeenChanged)));
}
}
void setCanonicalAliasAction(context) async {
final input = await showTextInputDialog(
context: context,
title: L10n.of(context).setInvitationLink,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
useRootNavigator: false,
textFields: [
DialogTextField(
hintText: '#localpart:domain',
initialText: L10n.of(context).alias.toLowerCase(),
)
],
);
if (input == null) return;
final room = Matrix.of(context).client.getRoomById(widget.roomId);
final domain = room.client.userID.domain;
final canonicalAlias = '%23' + input.single + '%3A' + domain;
final aliasEvent = room.getState('m.room.aliases', domain);
final aliases =
aliasEvent != null ? aliasEvent.content['aliases'] ?? [] : [];
if (aliases.indexWhere((s) => s == canonicalAlias) == -1) {
final newAliases = List<String>.from(aliases);
newAliases.add(canonicalAlias);
final response = await showFutureLoadingDialog(
context: context,
future: () => room.client.requestRoomAliasInformation(canonicalAlias),
);
if (response.error != null) {
final success = await showFutureLoadingDialog(
context: context,
future: () => room.client.createRoomAlias(canonicalAlias, room.id),
);
if (success.error != null) return;
}
}
await showFutureLoadingDialog(
context: context,
future: () => room.client.sendState(room.id, 'm.room.canonical_alias', {
'alias': input.single,
}),
);
}
void setTopicAction() async {
final room = Matrix.of(context).client.getRoomById(widget.roomId);
final input = await showTextInputDialog(
context: context,
title: L10n.of(context).setGroupDescription,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
useRootNavigator: false,
textFields: [
DialogTextField(
hintText: L10n.of(context).setGroupDescription,
initialText: room.topic,
minLines: 1,
maxLines: 4,
)
],
);
if (input == null) return;
final success = await showFutureLoadingDialog(
context: context,
future: () => room.setDescription(input.single),
);
if (success.error == null) {
AdaptivePageLayout.of(context).showSnackBar(SnackBar(
content: Text(L10n.of(context).groupDescriptionHasBeenChanged)));
}
}
void setGuestAccessAction(GuestAccess guestAccess) => showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context)
.client
.getRoomById(widget.roomId)
.setGuestAccess(guestAccess),
);
void setHistoryVisibilityAction(HistoryVisibility historyVisibility) =>
showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context)
.client
.getRoomById(widget.roomId)
.setHistoryVisibility(historyVisibility),
);
void setJoinRulesAction(JoinRules joinRule) => showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context)
.client
.getRoomById(widget.roomId)
.setJoinRules(joinRule),
);
void goToEmoteSettings() async {
final room = Matrix.of(context).client.getRoomById(widget.roomId);
// okay, we need to test if there are any emote state events other than the default one
// if so, we need to be directed to a selection screen for which pack we want to look at
// otherwise, we just open the normal one.
if ((room.states['im.ponies.room_emotes'] ?? <String, Event>{})
.keys
.any((String s) => s.isNotEmpty)) {
await AdaptivePageLayout.of(context)
.pushNamed('/rooms/${room.id}/emotes');
} else {
await AdaptivePageLayout.of(context)
.pushNamed('/settings/emotes', arguments: {'room': room});
}
}
void setAvatarAction() async {
MatrixFile file;
if (PlatformInfos.isMobile) {
final result = await ImagePicker().getImage(
source: ImageSource.gallery,
imageQuality: 50,
maxWidth: 1600,
maxHeight: 1600);
if (result == null) return;
file = MatrixFile(
bytes: await result.readAsBytes(),
name: result.path,
);
} else {
final result = await FilePickerCross.importFromStorage(
type: FileTypeCross.image,
);
if (result == null) return;
file = MatrixFile(
bytes: result.toUint8List(),
name: result.fileName,
);
}
final room = Matrix.of(context).client.getRoomById(widget.roomId);
final success = await showFutureLoadingDialog(
context: context,
future: () => room.setAvatar(file),
);
if (success.error == null) {
AdaptivePageLayout.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).avatarHasBeenChanged)));
}
}
void requestMoreMembersAction() async {
final room = Matrix.of(context).client.getRoomById(widget.roomId);
final participants = await showFutureLoadingDialog(
context: context, future: () => room.requestParticipants());
if (participants.error == null) {
setState(() => members = participants.result);
}
}
@override
Widget build(BuildContext context) => ChatDetailsView(this);
}

@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'matrix_file_extension.dart'; import 'matrix_file_extension.dart';
import '../controllers/image_viewer_controller.dart'; import '../views/image_viewer.dart';
extension LocalizedBody on Event { extension LocalizedBody on Event {
void openFile(BuildContext context, {bool downloadOnly = false}) async { void openFile(BuildContext context, {bool downloadOnly = false}) async {

@ -1,5 +1,5 @@
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/archive_view.dart'; import 'package:fluffychat/views/ui/archive_ui.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -19,5 +19,5 @@ class ArchiveController extends State<Archive> {
void forgetAction(int i) => setState(() => archive.removeAt(i)); void forgetAction(int i) => setState(() => archive.removeAt(i));
@override @override
Widget build(BuildContext context) => ArchiveView(this); Widget build(BuildContext context) => ArchiveUI(this);
} }

File diff suppressed because it is too large Load Diff

@ -1,369 +1,223 @@
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/controllers/chat_details_controller.dart';
import 'package:fluffychat/views/widgets/avatar.dart';
import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/widgets/chat_settings_popup_menu.dart'; import 'package:file_picker_cross/file_picker_cross.dart';
import 'package:fluffychat/views/widgets/content_banner.dart'; import 'package:fluffychat/views/ui/chat_details_ui.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:fluffychat/views/widgets/list_items/participant_list_item.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/utils/matrix_locals.dart'; import 'package:fluffychat/utils/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix_link_text/link_text.dart'; import 'package:image_picker/image_picker.dart';
import '../utils/url_launcher.dart'; class ChatDetails extends StatefulWidget {
final String roomId;
class ChatDetailsView extends StatelessWidget { const ChatDetails(this.roomId);
final ChatDetailsController controller;
const ChatDetailsView(this.controller, {Key key}) : super(key: key);
@override @override
Widget build(BuildContext context) { ChatDetailsController createState() => ChatDetailsController();
final room = }
Matrix.of(context).client.getRoomById(controller.widget.roomId);
if (room == null) { class ChatDetailsController extends State<ChatDetails> {
return Scaffold( List<User> members;
appBar: AppBar(
leading: BackButton(), @override
title: Text(L10n.of(context).oopsSomethingWentWrong), void initState() {
super.initState();
members ??=
Matrix.of(context).client.getRoomById(widget.roomId).getParticipants();
}
void setDisplaynameAction() async {
final room = Matrix.of(context).client.getRoomById(widget.roomId);
final input = await showTextInputDialog(
context: context,
title: L10n.of(context).changeTheNameOfTheGroup,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
useRootNavigator: false,
textFields: [
DialogTextField(
initialText: room.getLocalizedDisplayname(
MatrixLocals(
L10n.of(context),
), ),
body: Center(
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
), ),
)
],
);
if (input == null) return;
final success = await showFutureLoadingDialog(
context: context,
future: () => room.setName(input.single),
);
if (success.error == null) {
AdaptivePageLayout.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).displaynameHasBeenChanged)));
}
}
void setCanonicalAliasAction(context) async {
final input = await showTextInputDialog(
context: context,
title: L10n.of(context).setInvitationLink,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
useRootNavigator: false,
textFields: [
DialogTextField(
hintText: '#localpart:domain',
initialText: L10n.of(context).alias.toLowerCase(),
)
],
);
if (input == null) return;
final room = Matrix.of(context).client.getRoomById(widget.roomId);
final domain = room.client.userID.domain;
final canonicalAlias = '%23' + input.single + '%3A' + domain;
final aliasEvent = room.getState('m.room.aliases', domain);
final aliases =
aliasEvent != null ? aliasEvent.content['aliases'] ?? [] : [];
if (aliases.indexWhere((s) => s == canonicalAlias) == -1) {
final newAliases = List<String>.from(aliases);
newAliases.add(canonicalAlias);
final response = await showFutureLoadingDialog(
context: context,
future: () => room.client.requestRoomAliasInformation(canonicalAlias),
);
if (response.error != null) {
final success = await showFutureLoadingDialog(
context: context,
future: () => room.client.createRoomAlias(canonicalAlias, room.id),
);
if (success.error != null) return;
}
}
await showFutureLoadingDialog(
context: context,
future: () => room.client.sendState(room.id, 'm.room.canonical_alias', {
'alias': input.single,
}),
); );
} }
controller.members.removeWhere((u) => u.membership == Membership.leave); void setTopicAction() async {
final actualMembersCount = final room = Matrix.of(context).client.getRoomById(widget.roomId);
room.mInvitedMemberCount + room.mJoinedMemberCount; final input = await showTextInputDialog(
final canRequestMoreMembers = context: context,
controller.members.length < actualMembersCount; title: L10n.of(context).setGroupDescription,
return StreamBuilder( okLabel: L10n.of(context).ok,
stream: room.onUpdate.stream, cancelLabel: L10n.of(context).cancel,
builder: (context, snapshot) { useRootNavigator: false,
return Scaffold( textFields: [
body: NestedScrollView( DialogTextField(
headerSliverBuilder: hintText: L10n.of(context).setGroupDescription,
(BuildContext context, bool innerBoxIsScrolled) => <Widget>[ initialText: room.topic,
SliverAppBar( minLines: 1,
elevation: Theme.of(context).appBarTheme.elevation, maxLines: 4,
leading: BackButton(),
expandedHeight: 300.0,
floating: true,
pinned: true,
actions: <Widget>[
if (room.canonicalAlias?.isNotEmpty ?? false)
IconButton(
tooltip: L10n.of(context).share,
icon: Icon(Icons.share_outlined),
onPressed: () => FluffyShare.share(
AppConfig.inviteLinkPrefix + room.canonicalAlias,
context),
),
ChatSettingsPopupMenu(room, false)
],
title: Text(
room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context))),
style: TextStyle(
color: Theme.of(context)
.appBarTheme
.textTheme
.headline6
.color)),
backgroundColor: Theme.of(context).appBarTheme.color,
flexibleSpace: FlexibleSpaceBar(
background: ContentBanner(room.avatar,
onEdit: room.canSendEvent('m.room.avatar')
? controller.setAvatarAction
: null),
),
),
],
body: MaxWidthBody(
child: ListView.builder(
itemCount: controller.members.length +
1 +
(canRequestMoreMembers ? 1 : 0),
itemBuilder: (BuildContext context, int i) => i == 0
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
ListTile(
leading: room.canSendEvent('m.room.topic')
? CircleAvatar(
backgroundColor: Theme.of(context)
.scaffoldBackgroundColor,
foregroundColor: Colors.grey,
radius: Avatar.defaultSize / 2,
child: Icon(Icons.edit_outlined),
) )
: null,
title: Text(
'${L10n.of(context).groupDescription}:',
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold)),
subtitle: LinkText(
text: room.topic?.isEmpty ?? true
? L10n.of(context).addGroupDescription
: room.topic,
linkStyle: TextStyle(color: Colors.blueAccent),
textStyle: TextStyle(
fontSize: 14,
color: Theme.of(context)
.textTheme
.bodyText2
.color,
),
onLinkTap: (url) =>
UrlLauncher(context, url).launchUrl(),
),
onTap: room.canSendEvent('m.room.topic')
? controller.setTopicAction
: null,
),
Divider(thickness: 1),
ListTile(
title: Text(
L10n.of(context).settings,
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
),
),
if (room.canSendEvent('m.room.name'))
ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.people_outlined),
),
title: Text(
L10n.of(context).changeTheNameOfTheGroup),
subtitle: Text(room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)))),
onTap: controller.setDisplaynameAction,
),
if (room.canSendEvent('m.room.canonical_alias') &&
room.joinRules == JoinRules.public)
ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.link_outlined),
),
onTap: () =>
controller.setCanonicalAliasAction(context),
title: Text(L10n.of(context).setInvitationLink),
subtitle: Text(
(room.canonicalAlias?.isNotEmpty ?? false)
? room.canonicalAlias
: L10n.of(context).none),
),
ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.insert_emoticon_outlined),
),
title: Text(L10n.of(context).emoteSettings),
subtitle: Text(L10n.of(context).setCustomEmotes),
onTap: controller.goToEmoteSettings,
),
PopupMenuButton(
onSelected: controller.setJoinRulesAction,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<JoinRules>>[
if (room.canChangeJoinRules)
PopupMenuItem<JoinRules>(
value: JoinRules.public,
child: Text(JoinRules.public
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
if (room.canChangeJoinRules)
PopupMenuItem<JoinRules>(
value: JoinRules.invite,
child: Text(JoinRules.invite
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
], ],
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context)
.scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.public_outlined)),
title: Text(L10n.of(context)
.whoIsAllowedToJoinThisGroup),
subtitle: Text(
room.joinRules.getLocalizedString(
MatrixLocals(L10n.of(context))),
),
),
),
PopupMenuButton(
onSelected: controller.setHistoryVisibilityAction,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<HistoryVisibility>>[
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.invited,
child: Text(HistoryVisibility.invited
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.joined,
child: Text(HistoryVisibility.joined
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.shared,
child: Text(HistoryVisibility.shared
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.world_readable,
child: Text(HistoryVisibility.world_readable
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
],
child: ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.visibility_outlined),
),
title: Text(L10n.of(context)
.visibilityOfTheChatHistory),
subtitle: Text(
room.historyVisibility.getLocalizedString(
MatrixLocals(L10n.of(context))),
),
),
),
if (room.joinRules == JoinRules.public)
PopupMenuButton(
onSelected: controller.setGuestAccessAction,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<GuestAccess>>[
if (room.canChangeGuestAccess)
PopupMenuItem<GuestAccess>(
value: GuestAccess.can_join,
child: Text(
GuestAccess.can_join.getLocalizedString(
MatrixLocals(L10n.of(context))),
),
),
if (room.canChangeGuestAccess)
PopupMenuItem<GuestAccess>(
value: GuestAccess.forbidden,
child: Text(
GuestAccess.forbidden
.getLocalizedString(
MatrixLocals(L10n.of(context))),
),
),
],
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context)
.scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.info_outline),
),
title: Text(
L10n.of(context).areGuestsAllowedToJoin),
subtitle: Text(
room.guestAccess.getLocalizedString(
MatrixLocals(L10n.of(context))),
),
),
),
ListTile(
title: Text(L10n.of(context).editChatPermissions),
subtitle: Text(
L10n.of(context).whoCanPerformWhichAction),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.edit_attributes_outlined),
),
onTap: () => AdaptivePageLayout.of(context)
.pushNamed('/rooms/${room.id}/permissions'),
),
Divider(thickness: 1),
ListTile(
title: Text(
actualMembersCount > 1
? L10n.of(context).countParticipants(
actualMembersCount.toString())
: L10n.of(context).emptyChat,
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
),
),
room.canInvite
? ListTile(
title: Text(L10n.of(context).inviteContact),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).primaryColor,
foregroundColor: Colors.white,
radius: Avatar.defaultSize / 2,
child: Icon(Icons.add_outlined),
),
onTap: () => AdaptivePageLayout.of(context)
.pushNamed('/rooms/${room.id}/invite'),
)
: Container(),
],
)
: i < controller.members.length + 1
? ParticipantListItem(controller.members[i - 1])
: ListTile(
title: Text(L10n.of(context)
.loadCountMoreParticipants(
(actualMembersCount -
controller.members.length)
.toString())),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
child: Icon(
Icons.refresh,
color: Colors.grey,
),
),
onTap: controller.requestMoreMembersAction,
),
),
),
),
); );
}); if (input == null) return;
final success = await showFutureLoadingDialog(
context: context,
future: () => room.setDescription(input.single),
);
if (success.error == null) {
AdaptivePageLayout.of(context).showSnackBar(SnackBar(
content: Text(L10n.of(context).groupDescriptionHasBeenChanged)));
} }
}
void setGuestAccessAction(GuestAccess guestAccess) => showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context)
.client
.getRoomById(widget.roomId)
.setGuestAccess(guestAccess),
);
void setHistoryVisibilityAction(HistoryVisibility historyVisibility) =>
showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context)
.client
.getRoomById(widget.roomId)
.setHistoryVisibility(historyVisibility),
);
void setJoinRulesAction(JoinRules joinRule) => showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context)
.client
.getRoomById(widget.roomId)
.setJoinRules(joinRule),
);
void goToEmoteSettings() async {
final room = Matrix.of(context).client.getRoomById(widget.roomId);
// okay, we need to test if there are any emote state events other than the default one
// if so, we need to be directed to a selection screen for which pack we want to look at
// otherwise, we just open the normal one.
if ((room.states['im.ponies.room_emotes'] ?? <String, Event>{})
.keys
.any((String s) => s.isNotEmpty)) {
await AdaptivePageLayout.of(context)
.pushNamed('/rooms/${room.id}/emotes');
} else {
await AdaptivePageLayout.of(context)
.pushNamed('/settings/emotes', arguments: {'room': room});
}
}
void setAvatarAction() async {
MatrixFile file;
if (PlatformInfos.isMobile) {
final result = await ImagePicker().getImage(
source: ImageSource.gallery,
imageQuality: 50,
maxWidth: 1600,
maxHeight: 1600);
if (result == null) return;
file = MatrixFile(
bytes: await result.readAsBytes(),
name: result.path,
);
} else {
final result = await FilePickerCross.importFromStorage(
type: FileTypeCross.image,
);
if (result == null) return;
file = MatrixFile(
bytes: result.toUint8List(),
name: result.fileName,
);
}
final room = Matrix.of(context).client.getRoomById(widget.roomId);
final success = await showFutureLoadingDialog(
context: context,
future: () => room.setAvatar(file),
);
if (success.error == null) {
AdaptivePageLayout.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).avatarHasBeenChanged)));
}
}
void requestMoreMembersAction() async {
final room = Matrix.of(context).client.getRoomById(widget.roomId);
final participants = await showFutureLoadingDialog(
context: context, future: () => room.requestParticipants());
if (participants.error == null) {
setState(() => members = participants.result);
}
}
@override
Widget build(BuildContext context) => ChatDetailsUI(this);
} }

@ -1,9 +1,9 @@
import 'package:famedlysdk/encryption.dart'; import 'package:famedlysdk/encryption.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/chat_encryption_settings_view.dart'; import 'package:fluffychat/views/ui/chat_encryption_settings_ui.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../views/widgets/dialogs/key_verification_dialog.dart'; import 'widgets/dialogs/key_verification_dialog.dart';
class ChatEncryptionSettings extends StatefulWidget { class ChatEncryptionSettings extends StatefulWidget {
final String id; final String id;
@ -61,5 +61,5 @@ class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
} }
@override @override
Widget build(BuildContext context) => ChatEncryptionSettingsView(this); Widget build(BuildContext context) => ChatEncryptionSettingsUI(this);
} }

@ -5,7 +5,7 @@ import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/views/chat_list_view.dart'; import 'package:fluffychat/views/ui/chat_list_ui.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
@ -13,7 +13,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import '../views/widgets/matrix.dart'; import 'widgets/matrix.dart';
import '../utils/matrix_file_extension.dart'; import '../utils/matrix_file_extension.dart';
import '../utils/url_launcher.dart'; import '../utils/url_launcher.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -224,7 +224,7 @@ class ChatListController extends State<ChatList> {
} }
@override @override
Widget build(BuildContext context) => ChatListView(this); Widget build(BuildContext context) => ChatListUI(this);
} }
enum ChatListPopupMenuItemActions { enum ChatListPopupMenuItemActions {

@ -2,7 +2,7 @@ import 'dart:developer';
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:fluffychat/views/chat_permissions_settings_view.dart'; import 'package:fluffychat/views/ui/chat_permissions_settings_ui.dart';
import 'package:fluffychat/views/widgets/dialogs/permission_slider_dialog.dart'; import 'package:fluffychat/views/widgets/dialogs/permission_slider_dialog.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
@ -92,5 +92,5 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
} }
@override @override
Widget build(BuildContext context) => ChatPermissionsSettingsView(this); Widget build(BuildContext context) => ChatPermissionsSettingsUI(this);
} }

@ -1,13 +1,13 @@
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:famedlysdk/encryption/utils/key_verification.dart'; import 'package:famedlysdk/encryption/utils/key_verification.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/device_settings_view.dart'; import 'package:fluffychat/views/ui/device_settings_ui.dart';
import 'package:fluffychat/views/widgets/dialogs/key_verification_dialog.dart'; import 'package:fluffychat/views/widgets/dialogs/key_verification_dialog.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../views/widgets/matrix.dart'; import 'widgets/matrix.dart';
class DevicesSettings extends StatefulWidget { class DevicesSettings extends StatefulWidget {
@override @override
@ -136,5 +136,5 @@ class DevicesSettingsController extends State<DevicesSettings> {
..sort((a, b) => b.lastSeenTs.compareTo(a.lastSeenTs)); ..sort((a, b) => b.lastSeenTs.compareTo(a.lastSeenTs));
@override @override
Widget build(BuildContext context) => DevicesSettingsView(this); Widget build(BuildContext context) => DevicesSettingsUI(this);
} }

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/homeserver_picker_view.dart'; import 'package:fluffychat/views/ui/homeserver_picker_ui.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/setting_keys.dart';
@ -130,5 +130,5 @@ class HomeserverPickerController extends State<HomeserverPicker> {
} }
@override @override
Widget build(BuildContext context) => HomeserverPickerView(this); Widget build(BuildContext context) => HomeserverPickerUI(this);
} }

@ -1,7 +1,7 @@
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/views/image_viewer_view.dart'; import 'package:fluffychat/views/ui/image_viewer_ui.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -38,5 +38,5 @@ class ImageViewerController extends State<ImageViewer> {
} }
@override @override
Widget build(BuildContext context) => ImageViewerView(this); Widget build(BuildContext context) => ImageViewerUI(this);
} }

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/invitation_selection_view.dart'; import 'package:fluffychat/views/ui/invitation_selection_ui.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -116,5 +116,5 @@ class InvitationSelectionController extends State<InvitationSelection> {
} }
@override @override
Widget build(BuildContext context) => InvitationSelectionView(this); Widget build(BuildContext context) => InvitationSelectionUI(this);
} }

@ -1,6 +1,6 @@
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart' as sdk; import 'package:famedlysdk/famedlysdk.dart' as sdk;
import 'package:fluffychat/views/new_group_view.dart'; import 'package:fluffychat/views/ui/new_group_ui.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -40,5 +40,5 @@ class NewGroupController extends State<NewGroup> {
} }
@override @override
Widget build(BuildContext context) => NewGroupView(this); Widget build(BuildContext context) => NewGroupUI(this);
} }

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/views/new_private_chat_view.dart'; import 'package:fluffychat/views/ui/new_private_chat_ui.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -112,5 +112,5 @@ class NewPrivateChatController extends State<NewPrivateChat> {
); );
@override @override
Widget build(BuildContext context) => NewPrivateChatView(this); Widget build(BuildContext context) => NewPrivateChatUI(this);
} }

@ -1,7 +1,7 @@
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:file_picker_cross/file_picker_cross.dart'; import 'package:file_picker_cross/file_picker_cross.dart';
import 'package:fluffychat/views/sign_up_view.dart'; import 'package:fluffychat/views/ui/sign_up_ui.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@ -67,5 +67,5 @@ class SignUpController extends State<SignUp> {
} }
@override @override
Widget build(BuildContext context) => SignUpView(this); Widget build(BuildContext context) => SignUpUI(this);
} }

@ -2,7 +2,7 @@ import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/sign_up_password_view.dart'; import 'package:fluffychat/views/ui/sign_up_password_ui.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -127,5 +127,5 @@ class SignUpPasswordController extends State<SignUpPassword> {
} }
@override @override
Widget build(BuildContext context) => SignUpPasswordView(this); Widget build(BuildContext context) => SignUpPasswordUI(this);
} }

@ -1,13 +1,13 @@
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/controllers/archive_controller.dart'; import 'package:fluffychat/views/archive.dart';
import 'package:fluffychat/views/widgets/list_items/chat_list_item.dart'; import 'package:fluffychat/views/widgets/list_items/chat_list_item.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
class ArchiveView extends StatelessWidget { class ArchiveUI extends StatelessWidget {
final ArchiveController controller; final ArchiveController controller;
const ArchiveView(this.controller, {Key key}) : super(key: key); const ArchiveUI(this.controller, {Key key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

@ -0,0 +1,369 @@
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/views/chat_details.dart';
import 'package:fluffychat/views/widgets/avatar.dart';
import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/widgets/chat_settings_popup_menu.dart';
import 'package:fluffychat/views/widgets/content_banner.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:fluffychat/views/widgets/list_items/participant_list_item.dart';
import 'package:fluffychat/utils/matrix_locals.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix_link_text/link_text.dart';
import '../../utils/url_launcher.dart';
class ChatDetailsUI extends StatelessWidget {
final ChatDetailsController controller;
const ChatDetailsUI(this.controller, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final room =
Matrix.of(context).client.getRoomById(controller.widget.roomId);
if (room == null) {
return Scaffold(
appBar: AppBar(
leading: BackButton(),
title: Text(L10n.of(context).oopsSomethingWentWrong),
),
body: Center(
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
),
);
}
controller.members.removeWhere((u) => u.membership == Membership.leave);
final actualMembersCount =
room.mInvitedMemberCount + room.mJoinedMemberCount;
final canRequestMoreMembers =
controller.members.length < actualMembersCount;
return StreamBuilder(
stream: room.onUpdate.stream,
builder: (context, snapshot) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) => <Widget>[
SliverAppBar(
elevation: Theme.of(context).appBarTheme.elevation,
leading: BackButton(),
expandedHeight: 300.0,
floating: true,
pinned: true,
actions: <Widget>[
if (room.canonicalAlias?.isNotEmpty ?? false)
IconButton(
tooltip: L10n.of(context).share,
icon: Icon(Icons.share_outlined),
onPressed: () => FluffyShare.share(
AppConfig.inviteLinkPrefix + room.canonicalAlias,
context),
),
ChatSettingsPopupMenu(room, false)
],
title: Text(
room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context))),
style: TextStyle(
color: Theme.of(context)
.appBarTheme
.textTheme
.headline6
.color)),
backgroundColor: Theme.of(context).appBarTheme.color,
flexibleSpace: FlexibleSpaceBar(
background: ContentBanner(room.avatar,
onEdit: room.canSendEvent('m.room.avatar')
? controller.setAvatarAction
: null),
),
),
],
body: MaxWidthBody(
child: ListView.builder(
itemCount: controller.members.length +
1 +
(canRequestMoreMembers ? 1 : 0),
itemBuilder: (BuildContext context, int i) => i == 0
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
ListTile(
leading: room.canSendEvent('m.room.topic')
? CircleAvatar(
backgroundColor: Theme.of(context)
.scaffoldBackgroundColor,
foregroundColor: Colors.grey,
radius: Avatar.defaultSize / 2,
child: Icon(Icons.edit_outlined),
)
: null,
title: Text(
'${L10n.of(context).groupDescription}:',
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold)),
subtitle: LinkText(
text: room.topic?.isEmpty ?? true
? L10n.of(context).addGroupDescription
: room.topic,
linkStyle: TextStyle(color: Colors.blueAccent),
textStyle: TextStyle(
fontSize: 14,
color: Theme.of(context)
.textTheme
.bodyText2
.color,
),
onLinkTap: (url) =>
UrlLauncher(context, url).launchUrl(),
),
onTap: room.canSendEvent('m.room.topic')
? controller.setTopicAction
: null,
),
Divider(thickness: 1),
ListTile(
title: Text(
L10n.of(context).settings,
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
),
),
if (room.canSendEvent('m.room.name'))
ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.people_outlined),
),
title: Text(
L10n.of(context).changeTheNameOfTheGroup),
subtitle: Text(room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)))),
onTap: controller.setDisplaynameAction,
),
if (room.canSendEvent('m.room.canonical_alias') &&
room.joinRules == JoinRules.public)
ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.link_outlined),
),
onTap: () =>
controller.setCanonicalAliasAction(context),
title: Text(L10n.of(context).setInvitationLink),
subtitle: Text(
(room.canonicalAlias?.isNotEmpty ?? false)
? room.canonicalAlias
: L10n.of(context).none),
),
ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.insert_emoticon_outlined),
),
title: Text(L10n.of(context).emoteSettings),
subtitle: Text(L10n.of(context).setCustomEmotes),
onTap: controller.goToEmoteSettings,
),
PopupMenuButton(
onSelected: controller.setJoinRulesAction,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<JoinRules>>[
if (room.canChangeJoinRules)
PopupMenuItem<JoinRules>(
value: JoinRules.public,
child: Text(JoinRules.public
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
if (room.canChangeJoinRules)
PopupMenuItem<JoinRules>(
value: JoinRules.invite,
child: Text(JoinRules.invite
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
],
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context)
.scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.public_outlined)),
title: Text(L10n.of(context)
.whoIsAllowedToJoinThisGroup),
subtitle: Text(
room.joinRules.getLocalizedString(
MatrixLocals(L10n.of(context))),
),
),
),
PopupMenuButton(
onSelected: controller.setHistoryVisibilityAction,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<HistoryVisibility>>[
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.invited,
child: Text(HistoryVisibility.invited
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.joined,
child: Text(HistoryVisibility.joined
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.shared,
child: Text(HistoryVisibility.shared
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.world_readable,
child: Text(HistoryVisibility.world_readable
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
],
child: ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.visibility_outlined),
),
title: Text(L10n.of(context)
.visibilityOfTheChatHistory),
subtitle: Text(
room.historyVisibility.getLocalizedString(
MatrixLocals(L10n.of(context))),
),
),
),
if (room.joinRules == JoinRules.public)
PopupMenuButton(
onSelected: controller.setGuestAccessAction,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<GuestAccess>>[
if (room.canChangeGuestAccess)
PopupMenuItem<GuestAccess>(
value: GuestAccess.can_join,
child: Text(
GuestAccess.can_join.getLocalizedString(
MatrixLocals(L10n.of(context))),
),
),
if (room.canChangeGuestAccess)
PopupMenuItem<GuestAccess>(
value: GuestAccess.forbidden,
child: Text(
GuestAccess.forbidden
.getLocalizedString(
MatrixLocals(L10n.of(context))),
),
),
],
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context)
.scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.info_outline),
),
title: Text(
L10n.of(context).areGuestsAllowedToJoin),
subtitle: Text(
room.guestAccess.getLocalizedString(
MatrixLocals(L10n.of(context))),
),
),
),
ListTile(
title: Text(L10n.of(context).editChatPermissions),
subtitle: Text(
L10n.of(context).whoCanPerformWhichAction),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.edit_attributes_outlined),
),
onTap: () => AdaptivePageLayout.of(context)
.pushNamed('/rooms/${room.id}/permissions'),
),
Divider(thickness: 1),
ListTile(
title: Text(
actualMembersCount > 1
? L10n.of(context).countParticipants(
actualMembersCount.toString())
: L10n.of(context).emptyChat,
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
),
),
room.canInvite
? ListTile(
title: Text(L10n.of(context).inviteContact),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).primaryColor,
foregroundColor: Colors.white,
radius: Avatar.defaultSize / 2,
child: Icon(Icons.add_outlined),
),
onTap: () => AdaptivePageLayout.of(context)
.pushNamed('/rooms/${room.id}/invite'),
)
: Container(),
],
)
: i < controller.members.length + 1
? ParticipantListItem(controller.members[i - 1])
: ListTile(
title: Text(L10n.of(context)
.loadCountMoreParticipants(
(actualMembersCount -
controller.members.length)
.toString())),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
child: Icon(
Icons.refresh,
color: Colors.grey,
),
),
onTap: controller.requestMoreMembersAction,
),
),
),
),
);
});
}
}

@ -1,17 +1,16 @@
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/controllers/chat_encryption_settings_controller.dart'; import 'package:fluffychat/views/chat_encryption_settings.dart';
import 'package:fluffychat/views/widgets/avatar.dart'; import 'package:fluffychat/views/widgets/avatar.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart'; import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../utils/device_extension.dart'; import '../../utils/device_extension.dart';
class ChatEncryptionSettingsView extends StatelessWidget { class ChatEncryptionSettingsUI extends StatelessWidget {
final ChatEncryptionSettingsController controller; final ChatEncryptionSettingsController controller;
const ChatEncryptionSettingsView(this.controller, {Key key}) const ChatEncryptionSettingsUI(this.controller, {Key key}) : super(key: key);
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

@ -1,19 +1,19 @@
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/controllers/chat_list_controller.dart'; import 'package:fluffychat/views/chat_list.dart';
import 'package:fluffychat/views/widgets/connection_status_header.dart'; import 'package:fluffychat/views/widgets/connection_status_header.dart';
import 'package:fluffychat/views/widgets/list_items/chat_list_item.dart'; import 'package:fluffychat/views/widgets/list_items/chat_list_item.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'widgets/matrix.dart'; import '../widgets/matrix.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
class ChatListView extends StatelessWidget { class ChatListUI extends StatelessWidget {
final ChatListController controller; final ChatListController controller;
const ChatListView(this.controller, {Key key}) : super(key: key); const ChatListUI(this.controller, {Key key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

@ -1,4 +1,4 @@
import 'package:fluffychat/controllers/chat_permissions_settings_controller.dart'; import 'package:fluffychat/views/chat_permissions_settings.dart';
import 'package:fluffychat/views/widgets/list_items/permission_list_tile.dart'; import 'package:fluffychat/views/widgets/list_items/permission_list_tile.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart'; import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
@ -7,11 +7,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
class ChatPermissionsSettingsView extends StatelessWidget { class ChatPermissionsSettingsUI extends StatelessWidget {
final ChatPermissionsSettingsController controller; final ChatPermissionsSettingsController controller;
const ChatPermissionsSettingsView(this.controller, {Key key}) const ChatPermissionsSettingsUI(this.controller, {Key key}) : super(key: key);
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

747
lib/views/ui/chat_ui.dart Normal file

@ -0,0 +1,747 @@
import 'dart:math';
import 'dart:ui';
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/chat.dart';
import 'package:fluffychat/views/widgets/avatar.dart';
import 'package:fluffychat/views/widgets/chat_settings_popup_menu.dart';
import 'package:fluffychat/views/widgets/connection_status_header.dart';
import 'package:fluffychat/views/widgets/input_bar.dart';
import 'package:fluffychat/views/widgets/unread_badge_back_button.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/views/widgets/encryption_button.dart';
import 'package:fluffychat/views/widgets/list_items/message.dart';
import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:fluffychat/views/widgets/reply_content.dart';
import 'package:fluffychat/views/widgets/user_bottom_sheet.dart';
import 'package:fluffychat/config/app_emojis.dart';
import 'package:fluffychat/utils/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/room_status_extension.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:swipe_to_action/swipe_to_action.dart';
class ChatUI extends StatelessWidget {
final ChatController controller;
const ChatUI(this.controller, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
controller.matrix = Matrix.of(context);
final client = controller.matrix.client;
controller.room ??= client.getRoomById(controller.widget.id);
if (controller.room == null) {
return Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).oopsSomethingWentWrong),
),
body: Center(
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
),
);
}
controller.matrix.client.activeRoomId = controller.widget.id;
if (controller.room.membership == Membership.invite) {
showFutureLoadingDialog(
context: context, future: () => controller.room.join());
}
return Scaffold(
appBar: AppBar(
leading: controller.selectMode
? IconButton(
icon: Icon(Icons.close),
onPressed: controller.clearSelectedEvents,
tooltip: L10n.of(context).close,
)
: AdaptivePageLayout.of(context).columnMode(context)
? null
: UnreadBadgeBackButton(roomId: controller.widget.id),
titleSpacing:
AdaptivePageLayout.of(context).columnMode(context) ? null : 0,
title: controller.selectedEvents.isEmpty
? StreamBuilder(
stream: controller.room.onUpdate.stream,
builder: (context, snapshot) => ListTile(
leading: Avatar(
controller.room.avatar, controller.room.displayname),
contentPadding: EdgeInsets.zero,
onTap: controller.room.isDirectChat
? () => showModalBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
user: controller.room.getUserByMXIDSync(
controller.room.directChatMatrixID),
onMention: () => controller
.sendController.text +=
'${controller.room.directChatMatrixID} ',
),
)
: () => (!AdaptivePageLayout.of(context)
.columnMode(context) ||
AdaptivePageLayout.of(context)
.viewDataStack
.length <
3)
? AdaptivePageLayout.of(context).pushNamed(
'/rooms/${controller.room.id}/details')
: null,
title: Text(
controller.room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context))),
maxLines: 1),
subtitle: controller.room
.getLocalizedTypingText(context)
.isEmpty
? StreamBuilder<Object>(
stream: Matrix.of(context)
.client
.onPresence
.stream
.where((p) =>
p.senderId ==
controller.room.directChatMatrixID),
builder: (context, snapshot) => Text(
controller.room.getLocalizedStatus(context),
maxLines: 1,
//overflow: TextOverflow.ellipsis,
))
: Row(
children: <Widget>[
Icon(Icons.edit_outlined,
color: Theme.of(context).accentColor,
size: 13),
SizedBox(width: 4),
Expanded(
child: Text(
controller.room
.getLocalizedTypingText(context),
maxLines: 1,
style: TextStyle(
color: Theme.of(context).accentColor,
fontStyle: FontStyle.italic,
),
),
),
],
),
))
: Text(L10n.of(context)
.numberSelected(controller.selectedEvents.length.toString())),
actions: controller.selectMode
? <Widget>[
if (controller.selectedEvents.length == 1 &&
controller.selectedEvents.first.status > 0 &&
controller.selectedEvents.first.senderId == client.userID)
IconButton(
icon: Icon(Icons.edit_outlined),
tooltip: L10n.of(context).edit,
onPressed: controller.editSelectedEventAction,
),
PopupMenuButton(
onSelected: controller.onEventActionPopupMenuSelected,
itemBuilder: (_) => [
PopupMenuItem(
value: 'copy',
child: Text(L10n.of(context).copy),
),
if (controller.canRedactSelectedEvents)
PopupMenuItem(
value: 'redact',
child: Text(
L10n.of(context).redactMessage,
style: TextStyle(color: Colors.orange),
),
),
if (controller.selectedEvents.length == 1)
PopupMenuItem(
value: 'report',
child: Text(
L10n.of(context).reportMessage,
style: TextStyle(color: Colors.red),
),
),
],
),
]
: <Widget>[
if (controller.room.canSendDefaultStates)
IconButton(
tooltip: L10n.of(context).videoCall,
icon: Icon(Icons.video_call_outlined),
onPressed: controller.startCallAction,
),
ChatSettingsPopupMenu(
controller.room, !controller.room.isDirectChat),
],
),
floatingActionButton: controller.showScrollDownButton
? Padding(
padding: const EdgeInsets.only(bottom: 56.0),
child: FloatingActionButton(
onPressed: controller.scrollDown,
foregroundColor: Theme.of(context).textTheme.bodyText2.color,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
mini: true,
child: Icon(Icons.arrow_downward_outlined,
color: Theme.of(context).primaryColor),
),
)
: null,
body: Stack(
children: <Widget>[
if (Matrix.of(context).wallpaper != null)
Image.file(
Matrix.of(context).wallpaper,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
SafeArea(
child: Column(
children: <Widget>[
ConnectionStatusHeader(),
if (controller.room.getState(EventTypes.RoomTombstone) != null)
Container(
height: 72,
child: Material(
color: Theme.of(context).secondaryHeaderColor,
child: ListTile(
leading: CircleAvatar(
foregroundColor: Theme.of(context).accentColor,
backgroundColor: Theme.of(context).backgroundColor,
child: Icon(Icons.upgrade_outlined),
),
title: Text(
controller.room
.getState(EventTypes.RoomTombstone)
.parsedTombstoneContent
.body,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(L10n.of(context).goToTheNewRoom),
onTap: controller.goToNewRoomAction,
),
),
),
Expanded(
child: FutureBuilder<bool>(
future: controller.getTimeline(),
builder: (BuildContext context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
// 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 horizontalPadding = max(
0,
(MediaQuery.of(context).size.width -
FluffyThemes.columnWidth *
(AdaptivePageLayout.of(context)
.currentViewData
.rightView !=
null
? 4.5
: 3.5)) /
2)
.toDouble();
return ListView.custom(
padding: EdgeInsets.only(
top: 16,
left: horizontalPadding,
right: horizontalPadding,
),
reverse: true,
controller: controller.scrollController,
childrenDelegate: SliverChildBuilderDelegate(
(BuildContext context, int i) {
return i == controller.filteredEvents.length + 1
? controller.timeline.isRequestingHistory
? Container(
height: 50,
alignment: Alignment.center,
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
)
: controller.canLoadMore
? TextButton(
onPressed:
controller.requestHistory,
child: Text(
L10n.of(context).loadMore,
style: TextStyle(
color: Theme.of(context)
.primaryColor,
fontWeight: FontWeight.bold,
decoration:
TextDecoration.underline,
),
),
)
: Container()
: i == 0
? StreamBuilder(
stream: controller.room.onUpdate.stream,
builder: (_, __) {
final seenByText = controller.room
.getLocalizedSeenByText(
context,
controller.timeline,
controller.filteredEvents,
controller.unfolded,
);
return AnimatedContainer(
height: seenByText.isEmpty ? 0 : 24,
duration: seenByText.isEmpty
? Duration(milliseconds: 0)
: Duration(milliseconds: 300),
alignment: controller.filteredEvents
.first.senderId ==
client.userID
? Alignment.topRight
: Alignment.topLeft,
padding: EdgeInsets.only(
left: 8,
right: 8,
bottom: 8,
),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 4),
decoration: BoxDecoration(
color: Theme.of(context)
.scaffoldBackgroundColor
.withOpacity(0.8),
borderRadius:
BorderRadius.circular(4),
),
child: Text(
seenByText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context)
.accentColor,
),
),
),
);
},
)
: AutoScrollTag(
key: ValueKey(controller
.filteredEvents[i - 1].eventId),
index: i - 1,
controller: controller.scrollController,
child: Swipeable(
key: ValueKey(controller
.filteredEvents[i - 1].eventId),
background: 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,
onMention: () => controller
.sendController
.text +=
'${event.senderId} ',
),
),
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),
),
);
},
),
),
AnimatedContainer(
duration: Duration(milliseconds: 300),
height: (controller.editEvent == null &&
controller.replyEvent == null &&
controller.room.canSendDefaultMessages &&
controller.selectedEvents.length == 1)
? 56
: 0,
child: Material(
color: Theme.of(context).secondaryHeaderColor,
child: Builder(builder: (context) {
if (!(controller.editEvent == null &&
controller.replyEvent == null &&
controller.selectedEvents.length == 1)) {
return Container();
}
final emojis = List<String>.from(AppEmojis.emojis);
final allReactionEvents = controller.selectedEvents.first
.aggregatedEvents(
controller.timeline, RelationshipTypes.Reaction)
?.where((event) =>
event.senderId == event.room.client.userID &&
event.type == 'm.reaction');
allReactionEvents.forEach((event) {
try {
emojis.remove(event.content['m.relates_to']['key']);
} catch (_) {}
});
return ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: emojis.length + 1,
itemBuilder: (c, i) => i == emojis.length
? InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => controller
.pickEmojiAction(allReactionEvents),
child: Container(
width: 56,
height: 56,
alignment: Alignment.center,
child: Icon(Icons.add_outlined),
),
)
: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () =>
controller.sendEmojiAction(emojis[i]),
child: Container(
width: 56,
height: 56,
alignment: Alignment.center,
child: Text(
emojis[i],
style: TextStyle(fontSize: 30),
),
),
),
);
}),
),
),
AnimatedContainer(
duration: Duration(milliseconds: 300),
height: controller.editEvent != null ||
controller.replyEvent != null
? 56
: 0,
child: Material(
color: Theme.of(context).secondaryHeaderColor,
child: Row(
children: <Widget>[
IconButton(
tooltip: L10n.of(context).close,
icon: Icon(Icons.close),
onPressed: controller.cancelReplyEventAction,
),
Expanded(
child: controller.replyEvent != null
? ReplyContent(controller.replyEvent,
timeline: controller.timeline)
: _EditContent(controller.editEvent
?.getDisplayEvent(controller.timeline)),
),
],
),
),
),
Divider(
height: 1,
thickness: 1,
),
controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join
? Container(
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: controller.selectMode
? <Widget>[
Container(
height: 56,
child: TextButton(
onPressed: controller.forwardEventsAction,
child: Row(
children: <Widget>[
Icon(Icons
.keyboard_arrow_left_outlined),
Text(L10n.of(context).forward),
],
),
),
),
controller.selectedEvents.length == 1
? controller.selectedEvents.first
.getDisplayEvent(
controller.timeline)
.status >
0
? Container(
height: 56,
child: TextButton(
onPressed:
controller.replyAction,
child: Row(
children: <Widget>[
Text(
L10n.of(context).reply),
Icon(Icons
.keyboard_arrow_right),
],
),
),
)
: Container(
height: 56,
child: TextButton(
onPressed:
controller.sendAgainAction,
child: Row(
children: <Widget>[
Text(L10n.of(context)
.tryToSendAgain),
SizedBox(width: 4),
Icon(Icons.send_outlined,
size: 16),
],
),
),
)
: Container(),
]
: <Widget>[
if (controller.inputText.isEmpty)
Container(
height: 56,
alignment: Alignment.center,
child: PopupMenuButton<String>(
icon: Icon(Icons.add_outlined),
onSelected: controller
.onAddPopupMenuButtonSelected,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'file',
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
child: Icon(
Icons.attachment_outlined),
),
title: Text(
L10n.of(context).sendFile),
contentPadding: EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'image',
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
child:
Icon(Icons.image_outlined),
),
title: Text(
L10n.of(context).sendImage),
contentPadding: EdgeInsets.all(0),
),
),
if (PlatformInfos.isMobile)
PopupMenuItem<String>(
value: 'camera',
child: ListTile(
leading: CircleAvatar(
backgroundColor:
Colors.purple,
foregroundColor: Colors.white,
child: Icon(Icons
.camera_alt_outlined),
),
title: Text(L10n.of(context)
.openCamera),
contentPadding:
EdgeInsets.all(0),
),
),
if (PlatformInfos.isMobile)
PopupMenuItem<String>(
value: 'voice',
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Icon(
Icons.mic_none_outlined),
),
title: Text(L10n.of(context)
.voiceMessage),
contentPadding:
EdgeInsets.all(0),
),
),
],
),
),
Container(
height: 56,
alignment: Alignment.center,
child: EncryptionButton(controller.room),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 4.0),
child: InputBar(
room: controller.room,
minLines: 1,
maxLines: kIsWeb ? 1 : 8,
autofocus: !PlatformInfos.isMobile,
keyboardType: !PlatformInfos.isMobile
? TextInputType.text
: TextInputType.multiline,
onSubmitted:
controller.onInputBarSubmitted,
focusNode: controller.inputFocus,
controller: controller.sendController,
decoration: InputDecoration(
hintText:
L10n.of(context).writeAMessage,
hintMaxLines: 1,
border: InputBorder.none,
enabledBorder: InputBorder.none,
filled: false,
),
onChanged: controller.onInputBarChanged,
),
),
),
if (PlatformInfos.isMobile &&
controller.inputText.isEmpty)
Container(
height: 56,
alignment: Alignment.center,
child: IconButton(
tooltip: L10n.of(context).voiceMessage,
icon: Icon(Icons.mic_none_outlined),
onPressed:
controller.voiceMessageAction,
),
),
if (!PlatformInfos.isMobile ||
controller.inputText.isNotEmpty)
Container(
height: 56,
alignment: Alignment.center,
child: IconButton(
icon: Icon(Icons.send_outlined),
onPressed: controller.send,
tooltip: L10n.of(context).send,
),
),
],
),
)
: Container(),
],
),
),
],
),
);
}
}
class _EditContent extends StatelessWidget {
final Event event;
_EditContent(this.event);
@override
Widget build(BuildContext context) {
if (event == null) {
return Container();
}
return Row(
children: <Widget>[
Icon(
Icons.edit,
color: Theme.of(context).primaryColor,
),
Container(width: 15.0),
Text(
event?.getLocalizedBody(
MatrixLocals(L10n.of(context)),
withSenderNamePrefix: false,
hideReply: true,
) ??
'',
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
color: Theme.of(context).textTheme.bodyText2.color,
),
),
],
);
}
}

@ -1,14 +1,14 @@
import 'package:fluffychat/controllers/device_settings_controller.dart'; import 'package:fluffychat/views/device_settings.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart'; import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'widgets/list_items/user_device_list_item.dart'; import '../widgets/list_items/user_device_list_item.dart';
class DevicesSettingsView extends StatelessWidget { class DevicesSettingsUI extends StatelessWidget {
final DevicesSettingsController controller; final DevicesSettingsController controller;
const DevicesSettingsView(this.controller, {Key key}) : super(key: key); const DevicesSettingsUI(this.controller, {Key key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

@ -1,4 +1,4 @@
import '../controllers/homeserver_picker_controller.dart'; import '../homeserver_picker.dart';
import 'package:fluffychat/views/widgets/default_app_bar_search_field.dart'; import 'package:fluffychat/views/widgets/default_app_bar_search_field.dart';
import 'package:fluffychat/views/widgets/fluffy_banner.dart'; import 'package:fluffychat/views/widgets/fluffy_banner.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
@ -10,13 +10,10 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class HomeserverPickerView extends StatelessWidget { class HomeserverPickerUI extends StatelessWidget {
final HomeserverPickerController controller; final HomeserverPickerController controller;
const HomeserverPickerView( const HomeserverPickerUI(this.controller, {Key key}) : super(key: key);
this.controller, {
Key key,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

@ -1,12 +1,12 @@
import '../controllers/image_viewer_controller.dart'; import '../image_viewer.dart';
import 'package:fluffychat/views/widgets/image_bubble.dart'; import 'package:fluffychat/views/widgets/image_bubble.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
class ImageViewerView extends StatelessWidget { class ImageViewerUI extends StatelessWidget {
final ImageViewerController controller; final ImageViewerController controller;
const ImageViewerView(this.controller, {Key key}) : super(key: key); const ImageViewerUI(this.controller, {Key key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

@ -1,4 +1,4 @@
import 'package:fluffychat/controllers/invitation_selection_controller.dart'; import 'package:fluffychat/views/invitation_selection.dart';
import 'package:fluffychat/views/widgets/default_app_bar_search_field.dart'; import 'package:fluffychat/views/widgets/default_app_bar_search_field.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
@ -8,13 +8,10 @@ import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
class InvitationSelectionView extends StatelessWidget { class InvitationSelectionUI extends StatelessWidget {
final InvitationSelectionController controller; final InvitationSelectionController controller;
const InvitationSelectionView( const InvitationSelectionUI(this.controller, {Key key}) : super(key: key);
this.controller, {
Key key,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

@ -9,7 +9,7 @@ import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../utils/platform_infos.dart'; import '../../utils/platform_infos.dart';
import 'package:email_validator/email_validator.dart'; import 'package:email_validator/email_validator.dart';
class Login extends StatefulWidget { class Login extends StatefulWidget {

@ -1,15 +1,12 @@
import 'package:fluffychat/controllers/new_group_controller.dart'; import 'package:fluffychat/views/new_group.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart'; import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
class NewGroupView extends StatelessWidget { class NewGroupUI extends StatelessWidget {
final NewGroupController controller; final NewGroupController controller;
const NewGroupView( const NewGroupUI(this.controller, {Key key}) : super(key: key);
this.controller, {
Key key,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

@ -1,5 +1,5 @@
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:fluffychat/controllers/new_private_chat_controller.dart'; import 'package:fluffychat/views/new_private_chat.dart';
import 'package:fluffychat/views/widgets/avatar.dart'; import 'package:fluffychat/views/widgets/avatar.dart';
import 'package:fluffychat/views/widgets/contacts_list.dart'; import 'package:fluffychat/views/widgets/contacts_list.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart'; import 'package:fluffychat/views/widgets/max_width_body.dart';
@ -8,10 +8,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
class NewPrivateChatView extends StatelessWidget { class NewPrivateChatUI extends StatelessWidget {
final NewPrivateChatController controller; final NewPrivateChatController controller;
const NewPrivateChatView(this.controller, {Key key}) : super(key: key); const NewPrivateChatUI(this.controller, {Key key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

@ -11,7 +11,7 @@ import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import '../utils/localized_exception_extension.dart'; import '../../utils/localized_exception_extension.dart';
class SearchView extends StatefulWidget { class SearchView extends StatefulWidget {
final String alias; final String alias;

@ -13,7 +13,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import '../views/widgets/matrix.dart'; import '../widgets/matrix.dart';
class EmotesSettings extends StatefulWidget { class EmotesSettings extends StatefulWidget {
final Room room; final Room room;

@ -5,7 +5,7 @@ import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../views/widgets/matrix.dart'; import '../widgets/matrix.dart';
class SettingsIgnoreList extends StatefulWidget { class SettingsIgnoreList extends StatefulWidget {
final String initialUserId; final String initialUserId;

@ -9,9 +9,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:open_noti_settings/open_noti_settings.dart'; import 'package:open_noti_settings/open_noti_settings.dart';
import '../utils/localized_exception_extension.dart'; import '../../utils/localized_exception_extension.dart';
import '../views/widgets/matrix.dart'; import '../widgets/matrix.dart';
class NotificationSettingsItem { class NotificationSettingsItem {
final PushRuleKind type; final PushRuleKind type;

@ -7,8 +7,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import '../config/app_config.dart'; import '../../config/app_config.dart';
import '../views/widgets/matrix.dart'; import '../widgets/matrix.dart';
class SettingsStyle extends StatefulWidget { class SettingsStyle extends StatefulWidget {
@override @override

@ -21,11 +21,11 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../views/widgets/content_banner.dart'; import '../widgets/content_banner.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import '../views/widgets/matrix.dart'; import '../widgets/matrix.dart';
import '../config/app_config.dart'; import '../../config/app_config.dart';
import '../config/setting_keys.dart'; import '../../config/setting_keys.dart';
class Settings extends StatefulWidget { class Settings extends StatefulWidget {
@override @override

@ -1,16 +1,13 @@
import 'package:fluffychat/controllers/sign_up_password_controller.dart'; import 'package:fluffychat/views/sign_up_password.dart';
import 'package:fluffychat/views/widgets/one_page_card.dart'; import 'package:fluffychat/views/widgets/one_page_card.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
class SignUpPasswordView extends StatelessWidget { class SignUpPasswordUI extends StatelessWidget {
final SignUpPasswordController controller; final SignUpPasswordController controller;
const SignUpPasswordView( const SignUpPasswordUI(this.controller, {Key key}) : super(key: key);
this.controller, {
Key key,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

@ -1,5 +1,5 @@
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:fluffychat/controllers/sign_up_controller.dart'; import 'package:fluffychat/views/sign_up.dart';
import 'package:fluffychat/views/widgets/fluffy_banner.dart'; import 'package:fluffychat/views/widgets/fluffy_banner.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
@ -8,13 +8,10 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
class SignUpView extends StatelessWidget { class SignUpUI extends StatelessWidget {
final SignUpController controller; final SignUpController controller;
const SignUpView( const SignUpUI(this.controller, {Key key}) : super(key: key);
this.controller, {
Key key,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

@ -1,5 +1,5 @@
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/controllers/image_viewer_controller.dart'; import 'package:fluffychat/views/image_viewer.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart';

@ -19,7 +19,7 @@ import 'package:provider/provider.dart';
import 'package:universal_html/prefer_universal/html.dart' as html; import 'package:universal_html/prefer_universal/html.dart' as html;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
/*import 'package:fluffychat/views/chat.dart'; /*import 'package:fluffychat/views/chat_ui.dart';
import 'package:fluffychat/app_config.dart'; import 'package:fluffychat/app_config.dart';
import 'package:dbus/dbus.dart'; import 'package:dbus/dbus.dart';
import 'package:desktop_notifications/desktop_notifications.dart';*/ import 'package:desktop_notifications/desktop_notifications.dart';*/

@ -1,4 +1,4 @@
import 'package:fluffychat/controllers/homeserver_picker_controller.dart'; import 'package:fluffychat/views/homeserver_picker.dart';
import 'package:fluffychat/main.dart'; import 'package:fluffychat/main.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';

@ -1,4 +1,4 @@
import 'package:fluffychat/controllers/sign_up_password_controller.dart'; import 'package:fluffychat/views/sign_up_password.dart';
import 'package:fluffychat/main.dart'; import 'package:fluffychat/main.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';

@ -1,4 +1,4 @@
import 'package:fluffychat/controllers/sign_up_controller.dart'; import 'package:fluffychat/views/sign_up.dart';
import 'package:fluffychat/main.dart'; import 'package:fluffychat/main.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';