refactor: Folder structure and MVC chat ui
This commit is contained in:
parent
cb18589a16
commit
cd9b4ee46b
47 changed files with 1648 additions and 1582 deletions
23
lib/views/archive.dart
Normal file
23
lib/views/archive.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/views/ui/archive_ui.dart';
|
||||
import 'package:fluffychat/views/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Archive extends StatefulWidget {
|
||||
@override
|
||||
ArchiveController createState() => ArchiveController();
|
||||
}
|
||||
|
||||
class ArchiveController extends State<Archive> {
|
||||
List<Room> archive;
|
||||
|
||||
Future<List<Room>> getArchive(BuildContext context) async {
|
||||
if (archive != null) return archive;
|
||||
return await Matrix.of(context).client.archive;
|
||||
}
|
||||
|
||||
void forgetAction(int i) => setState(() => archive.removeAt(i));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ArchiveUI(this);
|
||||
}
|
||||
1075
lib/views/chat.dart
1075
lib/views/chat.dart
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: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: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:file_picker_cross/file_picker_cross.dart';
|
||||
import 'package:fluffychat/views/ui/chat_details_ui.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: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 {
|
||||
final ChatDetailsController controller;
|
||||
|
||||
const ChatDetailsView(this.controller, {Key key}) : super(key: key);
|
||||
const ChatDetails(this.roomId);
|
||||
|
||||
@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),
|
||||
),
|
||||
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);
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
65
lib/views/chat_encryption_settings.dart
Normal file
65
lib/views/chat_encryption_settings.dart
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/views/ui/chat_encryption_settings_ui.dart';
|
||||
import 'package:fluffychat/views/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'widgets/dialogs/key_verification_dialog.dart';
|
||||
|
||||
class ChatEncryptionSettings extends StatefulWidget {
|
||||
final String id;
|
||||
|
||||
const ChatEncryptionSettings(this.id, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ChatEncryptionSettingsController createState() =>
|
||||
ChatEncryptionSettingsController();
|
||||
}
|
||||
|
||||
class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
|
||||
Future<void> onSelected(
|
||||
BuildContext context, String action, DeviceKeys key) async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.id);
|
||||
final unblock = () async {
|
||||
if (key.blocked) {
|
||||
await key.setBlocked(false);
|
||||
}
|
||||
};
|
||||
switch (action) {
|
||||
case 'verify':
|
||||
await unblock();
|
||||
final req = key.startVerification();
|
||||
req.onUpdate = () {
|
||||
if (req.state == KeyVerificationState.done) {
|
||||
setState(() => null);
|
||||
}
|
||||
};
|
||||
await KeyVerificationDialog(request: req).show(context);
|
||||
break;
|
||||
case 'verify_user':
|
||||
await unblock();
|
||||
final req =
|
||||
await room.client.userDeviceKeys[key.userId].startVerification();
|
||||
req.onUpdate = () {
|
||||
if (req.state == KeyVerificationState.done) {
|
||||
setState(() => null);
|
||||
}
|
||||
};
|
||||
await KeyVerificationDialog(request: req).show(context);
|
||||
break;
|
||||
case 'block':
|
||||
if (key.directVerified) {
|
||||
await key.setVerified(false);
|
||||
}
|
||||
await key.setBlocked(true);
|
||||
setState(() => null);
|
||||
break;
|
||||
case 'unblock':
|
||||
await unblock();
|
||||
setState(() => null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatEncryptionSettingsUI(this);
|
||||
}
|
||||
236
lib/views/chat_list.dart
Normal file
236
lib/views/chat_list.dart
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/views/ui/chat_list_ui.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'widgets/matrix.dart';
|
||||
import '../utils/matrix_file_extension.dart';
|
||||
import '../utils/url_launcher.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
enum SelectMode { normal, share, select }
|
||||
enum PopupMenuAction { settings, invite, newGroup, setStatus, archive }
|
||||
|
||||
class ChatList extends StatefulWidget {
|
||||
final String activeChat;
|
||||
|
||||
const ChatList({this.activeChat, Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ChatListController createState() => ChatListController();
|
||||
}
|
||||
|
||||
class ChatListController extends State<ChatList> {
|
||||
StreamSubscription _intentDataStreamSubscription;
|
||||
|
||||
StreamSubscription _intentFileStreamSubscription;
|
||||
|
||||
final selectedRoomIds = <String>{};
|
||||
|
||||
void _processIncomingSharedFiles(List<SharedMediaFile> files) {
|
||||
if (files?.isEmpty ?? true) return;
|
||||
AdaptivePageLayout.of(context).popUntilIsFirst();
|
||||
final file = File(files.first.path);
|
||||
|
||||
Matrix.of(context).shareContent = {
|
||||
'msgtype': 'chat.fluffy.shared_file',
|
||||
'file': MatrixFile(
|
||||
bytes: file.readAsBytesSync(),
|
||||
name: file.path,
|
||||
).detectFileType,
|
||||
};
|
||||
}
|
||||
|
||||
void _processIncomingSharedText(String text) {
|
||||
if (text == null) return;
|
||||
AdaptivePageLayout.of(context).popUntilIsFirst();
|
||||
if (text.toLowerCase().startsWith(AppConfig.inviteLinkPrefix) ||
|
||||
(text.toLowerCase().startsWith(AppConfig.schemePrefix) &&
|
||||
!RegExp(r'\s').hasMatch(text))) {
|
||||
UrlLauncher(context, text).openMatrixToUrl();
|
||||
return;
|
||||
}
|
||||
Matrix.of(context).shareContent = {
|
||||
'msgtype': 'm.text',
|
||||
'body': text,
|
||||
};
|
||||
}
|
||||
|
||||
void _initReceiveSharingIntent() {
|
||||
if (!PlatformInfos.isMobile) return;
|
||||
|
||||
// For sharing images coming from outside the app while the app is in the memory
|
||||
_intentFileStreamSubscription = ReceiveSharingIntent.getMediaStream()
|
||||
.listen(_processIncomingSharedFiles, onError: print);
|
||||
|
||||
// For sharing images coming from outside the app while the app is closed
|
||||
ReceiveSharingIntent.getInitialMedia().then(_processIncomingSharedFiles);
|
||||
|
||||
// For sharing or opening urls/text coming from outside the app while the app is in the memory
|
||||
_intentDataStreamSubscription = ReceiveSharingIntent.getTextStream()
|
||||
.listen(_processIncomingSharedText, onError: print);
|
||||
|
||||
// For sharing or opening urls/text coming from outside the app while the app is closed
|
||||
ReceiveSharingIntent.getInitialText().then(_processIncomingSharedText);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_initReceiveSharingIntent();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_intentDataStreamSubscription?.cancel();
|
||||
_intentFileStreamSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void toggleSelection(String roomId) {
|
||||
setState(() => selectedRoomIds.contains(roomId)
|
||||
? selectedRoomIds.remove(roomId)
|
||||
: selectedRoomIds.add(roomId));
|
||||
}
|
||||
|
||||
Future<void> toggleUnread() {
|
||||
final room = Matrix.of(context).client.getRoomById(selectedRoomIds.single);
|
||||
return showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setUnread(!room.isUnread),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleFavouriteRoom() {
|
||||
final room = Matrix.of(context).client.getRoomById(selectedRoomIds.single);
|
||||
return showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setFavourite(!room.isFavourite),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleMuted() {
|
||||
final room = Matrix.of(context).client.getRoomById(selectedRoomIds.single);
|
||||
return showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setPushRuleState(
|
||||
room.pushRuleState == PushRuleState.notify
|
||||
? PushRuleState.mentions_only
|
||||
: PushRuleState.notify),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> archiveAction() async {
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).yes,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
) ==
|
||||
OkCancelResult.ok;
|
||||
if (!confirmed) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => _archiveSelectedRooms(),
|
||||
);
|
||||
setState(() => null);
|
||||
}
|
||||
|
||||
void setStatus() async {
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).setStatus,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context).statusExampleMessage,
|
||||
),
|
||||
]);
|
||||
if (input == null) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.sendPresence(
|
||||
Matrix.of(context).client.userID,
|
||||
PresenceType.online,
|
||||
statusMsg: input.single,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onPopupMenuSelect(action) {
|
||||
switch (action) {
|
||||
case PopupMenuAction.setStatus:
|
||||
setStatus();
|
||||
break;
|
||||
case PopupMenuAction.settings:
|
||||
AdaptivePageLayout.of(context).pushNamed('/settings');
|
||||
break;
|
||||
case PopupMenuAction.invite:
|
||||
FluffyShare.share(
|
||||
L10n.of(context).inviteText(Matrix.of(context).client.userID,
|
||||
'https://matrix.to/#/${Matrix.of(context).client.userID}'),
|
||||
context);
|
||||
break;
|
||||
case PopupMenuAction.newGroup:
|
||||
AdaptivePageLayout.of(context).pushNamed('/newgroup');
|
||||
break;
|
||||
case PopupMenuAction.archive:
|
||||
AdaptivePageLayout.of(context).pushNamed('/archive');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _archiveSelectedRooms() async {
|
||||
final client = Matrix.of(context).client;
|
||||
while (selectedRoomIds.isNotEmpty) {
|
||||
final roomId = selectedRoomIds.first;
|
||||
await client.getRoomById(roomId).leave();
|
||||
toggleSelection(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> waitForFirstSync() async {
|
||||
final client = Matrix.of(context).client;
|
||||
if (client.prevBatch?.isEmpty ?? true) {
|
||||
await client.onFirstSync.stream.first;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void cancelAction() {
|
||||
final selectMode = Matrix.of(context).shareContent != null
|
||||
? SelectMode.share
|
||||
: selectedRoomIds.isEmpty
|
||||
? SelectMode.normal
|
||||
: SelectMode.select;
|
||||
if (selectMode == SelectMode.share) {
|
||||
setState(() => Matrix.of(context).shareContent = null);
|
||||
} else {
|
||||
setState(() => selectedRoomIds.clear());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatListUI(this);
|
||||
}
|
||||
|
||||
enum ChatListPopupMenuItemActions {
|
||||
createGroup,
|
||||
discover,
|
||||
setStatus,
|
||||
inviteContact,
|
||||
settings,
|
||||
}
|
||||
96
lib/views/chat_permissions_settings.dart
Normal file
96
lib/views/chat_permissions_settings.dart
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:fluffychat/views/ui/chat_permissions_settings_ui.dart';
|
||||
import 'package:fluffychat/views/widgets/dialogs/permission_slider_dialog.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/views/widgets/matrix.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
||||
class ChatPermissionsSettings extends StatefulWidget {
|
||||
final String roomId;
|
||||
|
||||
const ChatPermissionsSettings(this.roomId, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ChatPermissionsSettingsController createState() =>
|
||||
ChatPermissionsSettingsController();
|
||||
}
|
||||
|
||||
class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
|
||||
void editPowerLevel(BuildContext context, String key, int currentLevel,
|
||||
{String category}) async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
if (!room.canSendEvent(EventTypes.RoomPowerLevels)) {
|
||||
AdaptivePageLayout.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(L10n.of(context).noPermission)));
|
||||
return;
|
||||
}
|
||||
final newLevel =
|
||||
await PermissionSliderDialog(initialPermission: currentLevel)
|
||||
.show(context);
|
||||
if (newLevel == null) return;
|
||||
final content = Map<String, dynamic>.from(
|
||||
room.getState(EventTypes.RoomPowerLevels).content);
|
||||
if (category != null) {
|
||||
if (!content.containsKey(category)) {
|
||||
content[category] = <String, dynamic>{};
|
||||
}
|
||||
content[category][key] = newLevel;
|
||||
} else {
|
||||
content[key] = newLevel;
|
||||
}
|
||||
inspect(content);
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
room.client.sendState(room.id, EventTypes.RoomPowerLevels, content),
|
||||
);
|
||||
}
|
||||
|
||||
Stream get onChanged => Matrix.of(context).client.onSync.stream.where(
|
||||
(e) =>
|
||||
(e?.rooms?.join?.containsKey(widget.roomId) ?? false) &&
|
||||
(e.rooms.join[widget.roomId]?.timeline?.events
|
||||
?.any((s) => s.type == EventTypes.RoomPowerLevels) ??
|
||||
false),
|
||||
);
|
||||
|
||||
void updateRoomAction(ServerCapabilities capabilities) async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
final String roomVersion =
|
||||
room.getState(EventTypes.RoomCreate).content['room_version'] ?? '1';
|
||||
final newVersion = await showConfirmationDialog<String>(
|
||||
context: context,
|
||||
title: L10n.of(context).replaceRoomWithNewerVersion,
|
||||
actions: capabilities.mRoomVersions.available.entries
|
||||
.where((r) => r.key != roomVersion)
|
||||
.map((version) => AlertDialogAction(
|
||||
key: version.key,
|
||||
label:
|
||||
'${version.key} (${version.value.toString().split('.').last})'))
|
||||
.toList(),
|
||||
);
|
||||
if (newVersion == null ||
|
||||
OkCancelResult.cancel ==
|
||||
await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
okLabel: L10n.of(context).yes,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
title: L10n.of(context).areYouSure,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.client.upgradeRoom(widget.roomId, newVersion),
|
||||
).then((_) => AdaptivePageLayout.of(context).pop());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatPermissionsSettingsUI(this);
|
||||
}
|
||||
140
lib/views/device_settings.dart
Normal file
140
lib/views/device_settings.dart
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:famedlysdk/encryption/utils/key_verification.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/views/ui/device_settings_ui.dart';
|
||||
import 'package:fluffychat/views/widgets/dialogs/key_verification_dialog.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'widgets/matrix.dart';
|
||||
|
||||
class DevicesSettings extends StatefulWidget {
|
||||
@override
|
||||
DevicesSettingsController createState() => DevicesSettingsController();
|
||||
}
|
||||
|
||||
class DevicesSettingsController extends State<DevicesSettings> {
|
||||
List<Device> devices;
|
||||
Future<bool> loadUserDevices(BuildContext context) async {
|
||||
if (devices != null) return true;
|
||||
devices = await Matrix.of(context).client.requestDevices();
|
||||
return true;
|
||||
}
|
||||
|
||||
void reload() => setState(() => devices = null);
|
||||
|
||||
bool loadingDeletingDevices = false;
|
||||
String errorDeletingDevices;
|
||||
|
||||
void removeDevicesAction(List<Device> devices) async {
|
||||
if (await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).yes,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
) ==
|
||||
OkCancelResult.cancel) return;
|
||||
final matrix = Matrix.of(context);
|
||||
final deviceIds = <String>[];
|
||||
for (final userDevice in devices) {
|
||||
deviceIds.add(userDevice.deviceId);
|
||||
}
|
||||
|
||||
try {
|
||||
setState(() {
|
||||
loadingDeletingDevices = true;
|
||||
errorDeletingDevices = null;
|
||||
});
|
||||
await matrix.client.uiaRequestBackground(
|
||||
(auth) => matrix.client.deleteDevices(
|
||||
deviceIds,
|
||||
auth: auth,
|
||||
),
|
||||
);
|
||||
reload();
|
||||
} catch (e, s) {
|
||||
Logs().v('Error while deleting devices', e, s);
|
||||
setState(() => errorDeletingDevices = e.toString());
|
||||
} finally {
|
||||
setState(() => loadingDeletingDevices = false);
|
||||
}
|
||||
}
|
||||
|
||||
void renameDeviceAction(Device device) async {
|
||||
final displayName = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).changeDeviceName,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: device.displayName,
|
||||
)
|
||||
],
|
||||
);
|
||||
if (displayName == null) return;
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context)
|
||||
.client
|
||||
.setDeviceMetadata(device.deviceId, displayName: displayName.single),
|
||||
);
|
||||
if (success.error == null) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
void verifyDeviceAction(Device device) async {
|
||||
final req = Matrix.of(context)
|
||||
.client
|
||||
.userDeviceKeys[Matrix.of(context).client.userID]
|
||||
.deviceKeys[device.deviceId]
|
||||
.startVerification();
|
||||
req.onUpdate = () {
|
||||
if ({KeyVerificationState.error, KeyVerificationState.done}
|
||||
.contains(req.state)) {
|
||||
setState(() => null);
|
||||
}
|
||||
};
|
||||
await KeyVerificationDialog(request: req).show(context);
|
||||
}
|
||||
|
||||
void blockDeviceAction(Device device) async {
|
||||
final key = Matrix.of(context)
|
||||
.client
|
||||
.userDeviceKeys[Matrix.of(context).client.userID]
|
||||
.deviceKeys[device.deviceId];
|
||||
if (key.directVerified) {
|
||||
await key.setVerified(false);
|
||||
}
|
||||
await key.setBlocked(true);
|
||||
setState(() => null);
|
||||
}
|
||||
|
||||
void unblockDeviceAction(Device device) async {
|
||||
final key = Matrix.of(context)
|
||||
.client
|
||||
.userDeviceKeys[Matrix.of(context).client.userID]
|
||||
.deviceKeys[device.deviceId];
|
||||
await key.setBlocked(false);
|
||||
setState(() => null);
|
||||
}
|
||||
|
||||
bool _isOwnDevice(Device userDevice) =>
|
||||
userDevice.deviceId == Matrix.of(context).client.deviceID;
|
||||
|
||||
Device get thisDevice => devices.firstWhere(
|
||||
_isOwnDevice,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
List<Device> get notThisDevice => List<Device>.from(devices)
|
||||
..removeWhere(_isOwnDevice)
|
||||
..sort((a, b) => b.lastSeenTs.compareTo(a.lastSeenTs));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => DevicesSettingsUI(this);
|
||||
}
|
||||
134
lib/views/homeserver_picker.dart
Normal file
134
lib/views/homeserver_picker.dart
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/views/ui/homeserver_picker_ui.dart';
|
||||
import 'package:fluffychat/views/widgets/matrix.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../utils/localized_exception_extension.dart';
|
||||
import 'package:universal_html/prefer_universal/html.dart' as html;
|
||||
|
||||
class HomeserverPicker extends StatefulWidget {
|
||||
@override
|
||||
HomeserverPickerController createState() => HomeserverPickerController();
|
||||
}
|
||||
|
||||
class HomeserverPickerController extends State<HomeserverPicker> {
|
||||
bool isLoading = false;
|
||||
String domain = AppConfig.defaultHomeserver;
|
||||
final TextEditingController homeserverController =
|
||||
TextEditingController(text: AppConfig.defaultHomeserver);
|
||||
StreamSubscription _intentDataStreamSubscription;
|
||||
|
||||
void _loginWithToken(String token) {
|
||||
if (token?.isEmpty ?? true) return;
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.login(
|
||||
type: AuthenticationTypes.token,
|
||||
token: token,
|
||||
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _processIncomingSharedText(String text) async {
|
||||
if (text == null || !text.startsWith(AppConfig.appOpenUrlScheme)) return;
|
||||
AdaptivePageLayout.of(context).popUntilIsFirst();
|
||||
final token = Uri.parse(text).queryParameters['loginToken'];
|
||||
_loginWithToken(token);
|
||||
}
|
||||
|
||||
void _initReceiveSharingContent() {
|
||||
if (!PlatformInfos.isMobile) return;
|
||||
_intentDataStreamSubscription =
|
||||
ReceiveSharingIntent.getTextStream().listen(_processIncomingSharedText);
|
||||
ReceiveSharingIntent.getInitialText().then(_processIncomingSharedText);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initReceiveSharingContent();
|
||||
if (kIsWeb) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final token =
|
||||
Uri.parse(html.window.location.href).queryParameters['loginToken'];
|
||||
_loginWithToken(token);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_intentDataStreamSubscription?.cancel();
|
||||
}
|
||||
|
||||
/// Starts an analysis of the given homeserver. It uses the current domain and
|
||||
/// makes sure that it is prefixed with https. Then it searches for the
|
||||
/// well-known information and forwards to the login page depending on the
|
||||
/// login type. For SSO login only the app opens the page and otherwise it
|
||||
/// forwards to the route `/signup`.
|
||||
void checkHomeserverAction() async {
|
||||
try {
|
||||
if (domain.isEmpty) throw L10n.of(context).changeTheHomeserver;
|
||||
var homeserver = domain;
|
||||
|
||||
if (!homeserver.startsWith('https://')) {
|
||||
homeserver = 'https://$homeserver';
|
||||
}
|
||||
|
||||
setState(() => isLoading = true);
|
||||
final wellKnown =
|
||||
await Matrix.of(context).client.checkHomeserver(homeserver);
|
||||
|
||||
var jitsi = wellKnown?.content
|
||||
?.tryGet<Map<String, dynamic>>('im.vector.riot.jitsi')
|
||||
?.tryGet<String>('preferredDomain');
|
||||
if (jitsi != null) {
|
||||
if (!jitsi.endsWith('/')) {
|
||||
jitsi += '/';
|
||||
}
|
||||
Logs().v('Found custom jitsi instance $jitsi');
|
||||
await Matrix.of(context)
|
||||
.store
|
||||
.setItem(SettingKeys.jitsiInstance, jitsi);
|
||||
AppConfig.jitsiInstance = jitsi;
|
||||
}
|
||||
|
||||
final loginTypes = await Matrix.of(context).client.requestLoginTypes();
|
||||
if (loginTypes.flows
|
||||
.any((flow) => flow.type == AuthenticationTypes.password)) {
|
||||
await AdaptivePageLayout.of(context)
|
||||
.pushNamed(AppConfig.enableRegistration ? '/signup' : '/login');
|
||||
} else if (loginTypes.flows
|
||||
.any((flow) => flow.type == AuthenticationTypes.sso)) {
|
||||
final redirectUrl = kIsWeb
|
||||
? html.window.location.href
|
||||
: AppConfig.appOpenUrlScheme.toLowerCase() + '://sso';
|
||||
await launch(
|
||||
'${Matrix.of(context).client.homeserver?.toString()}/_matrix/client/r0/login/sso/redirect?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}');
|
||||
}
|
||||
} catch (e) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text((e as Object).toLocalizedString(context))));
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => HomeserverPickerUI(this);
|
||||
}
|
||||
42
lib/views/image_viewer.dart
Normal file
42
lib/views/image_viewer.dart
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/views/ui/image_viewer_ui.dart';
|
||||
import 'package:fluffychat/views/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../utils/event_extension.dart';
|
||||
|
||||
class ImageViewer extends StatefulWidget {
|
||||
final Event event;
|
||||
final void Function() onLoaded;
|
||||
|
||||
const ImageViewer(this.event, {Key key, this.onLoaded}) : super(key: key);
|
||||
|
||||
@override
|
||||
ImageViewerController createState() => ImageViewerController();
|
||||
}
|
||||
|
||||
class ImageViewerController extends State<ImageViewer> {
|
||||
/// Forward this image to another room.
|
||||
void forwardAction() {
|
||||
Matrix.of(context).shareContent = widget.event.content;
|
||||
AdaptivePageLayout.of(context).popUntilIsFirst();
|
||||
}
|
||||
|
||||
/// Open this file with a system call.
|
||||
void openFileAction() => widget.event.openFile(context, downloadOnly: true);
|
||||
|
||||
/// Go back if user swiped it away
|
||||
void onInteractionEnds(ScaleEndDetails endDetails) {
|
||||
if (PlatformInfos.usesTouchscreen == false) {
|
||||
if (endDetails.velocity.pixelsPerSecond.dy >
|
||||
MediaQuery.of(context).size.height * 1.50) {
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ImageViewerUI(this);
|
||||
}
|
||||
120
lib/views/invitation_selection.dart
Normal file
120
lib/views/invitation_selection.dart
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/views/ui/invitation_selection_ui.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/views/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../utils/localized_exception_extension.dart';
|
||||
|
||||
class InvitationSelection extends StatefulWidget {
|
||||
final String roomId;
|
||||
const InvitationSelection(this.roomId, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
InvitationSelectionController createState() =>
|
||||
InvitationSelectionController();
|
||||
}
|
||||
|
||||
class InvitationSelectionController extends State<InvitationSelection> {
|
||||
TextEditingController controller = TextEditingController();
|
||||
String currentSearchTerm;
|
||||
bool loading = false;
|
||||
List<Profile> foundProfiles = [];
|
||||
Timer coolDown;
|
||||
|
||||
Future<List<User>> getContacts(BuildContext context) async {
|
||||
final client = Matrix.of(context).client;
|
||||
final room = client.getRoomById(widget.roomId);
|
||||
final participants = await room.requestParticipants();
|
||||
participants.removeWhere(
|
||||
(u) => ![Membership.join, Membership.invite].contains(u.membership),
|
||||
);
|
||||
final contacts = <User>[];
|
||||
final userMap = <String, bool>{};
|
||||
for (var i = 0; i < client.rooms.length; i++) {
|
||||
final roomUsers = client.rooms[i].getParticipants();
|
||||
|
||||
for (var j = 0; j < roomUsers.length; j++) {
|
||||
if (userMap[roomUsers[j].id] != true &&
|
||||
participants.indexWhere((u) => u.id == roomUsers[j].id) == -1) {
|
||||
contacts.add(roomUsers[j]);
|
||||
}
|
||||
userMap[roomUsers[j].id] = true;
|
||||
}
|
||||
}
|
||||
contacts.sort(
|
||||
(a, b) => a.calcDisplayname().toLowerCase().compareTo(
|
||||
b.calcDisplayname().toLowerCase(),
|
||||
),
|
||||
);
|
||||
return contacts;
|
||||
}
|
||||
|
||||
void inviteAction(BuildContext context, String id) async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.invite(id),
|
||||
);
|
||||
if (success.error == null) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(SnackBar(
|
||||
content: Text(L10n.of(context).contactHasBeenInvitedToTheGroup)));
|
||||
}
|
||||
}
|
||||
|
||||
void searchUserWithCoolDown(String text) async {
|
||||
coolDown?.cancel();
|
||||
coolDown = Timer(
|
||||
Duration(seconds: 1),
|
||||
() => searchUser(context, text),
|
||||
);
|
||||
}
|
||||
|
||||
void searchUser(BuildContext context, String text) async {
|
||||
coolDown?.cancel();
|
||||
if (text.isEmpty) {
|
||||
setState(() => foundProfiles = []);
|
||||
}
|
||||
currentSearchTerm = text;
|
||||
if (currentSearchTerm.isEmpty) return;
|
||||
if (loading) return;
|
||||
setState(() => loading = true);
|
||||
final matrix = Matrix.of(context);
|
||||
UserSearchResult response;
|
||||
try {
|
||||
response = await matrix.client.searchUser(text, limit: 10);
|
||||
} catch (e) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text((e as Object).toLocalizedString(context))));
|
||||
return;
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
setState(() {
|
||||
foundProfiles = List<Profile>.from(response.results);
|
||||
if (text.isValidMatrixId &&
|
||||
foundProfiles.indexWhere((profile) => text == profile.userId) == -1) {
|
||||
setState(() => foundProfiles = [
|
||||
Profile.fromJson({'user_id': text}),
|
||||
]);
|
||||
}
|
||||
final participants = Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(widget.roomId)
|
||||
.getParticipants()
|
||||
.where((user) =>
|
||||
[Membership.join, Membership.invite].contains(user.membership))
|
||||
.toList();
|
||||
foundProfiles.removeWhere((profile) =>
|
||||
participants.indexWhere((u) => u.id == profile.userId) != -1);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => InvitationSelectionUI(this);
|
||||
}
|
||||
44
lib/views/new_group.dart
Normal file
44
lib/views/new_group.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart' as sdk;
|
||||
import 'package:fluffychat/views/ui/new_group_ui.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/views/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NewGroup extends StatefulWidget {
|
||||
@override
|
||||
NewGroupController createState() => NewGroupController();
|
||||
}
|
||||
|
||||
class NewGroupController extends State<NewGroup> {
|
||||
TextEditingController controller = TextEditingController();
|
||||
bool publicGroup = false;
|
||||
|
||||
void setPublicGroup(bool b) => setState(() => publicGroup = b);
|
||||
|
||||
void submitAction([_]) async {
|
||||
final matrix = Matrix.of(context);
|
||||
final roomID = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => matrix.client.createRoom(
|
||||
preset: publicGroup
|
||||
? sdk.CreateRoomPreset.public_chat
|
||||
: sdk.CreateRoomPreset.private_chat,
|
||||
visibility: publicGroup ? sdk.Visibility.public : null,
|
||||
roomAliasName: publicGroup && controller.text.isNotEmpty
|
||||
? controller.text.trim().toLowerCase().replaceAll(' ', '_')
|
||||
: null,
|
||||
name: controller.text.isNotEmpty ? controller.text : null,
|
||||
),
|
||||
);
|
||||
AdaptivePageLayout.of(context).popUntilIsFirst();
|
||||
if (roomID != null) {
|
||||
await AdaptivePageLayout.of(context).pushNamed('/rooms/${roomID.result}');
|
||||
await AdaptivePageLayout.of(context)
|
||||
.pushNamed('/rooms/${roomID.result}/invite');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => NewGroupUI(this);
|
||||
}
|
||||
116
lib/views/new_private_chat.dart
Normal file
116
lib/views/new_private_chat.dart
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/views/ui/new_private_chat_ui.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/views/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class NewPrivateChat extends StatefulWidget {
|
||||
@override
|
||||
NewPrivateChatController createState() => NewPrivateChatController();
|
||||
}
|
||||
|
||||
class NewPrivateChatController extends State<NewPrivateChat> {
|
||||
TextEditingController controller = TextEditingController();
|
||||
final formKey = GlobalKey<FormState>();
|
||||
bool loading = false;
|
||||
String currentSearchTerm;
|
||||
List<Profile> foundProfiles = [];
|
||||
Timer coolDown;
|
||||
Profile get foundProfile =>
|
||||
foundProfiles.firstWhere((user) => user.userId == '@$currentSearchTerm',
|
||||
orElse: () => null);
|
||||
bool get correctMxId =>
|
||||
foundProfiles
|
||||
.indexWhere((user) => user.userId == '@$currentSearchTerm') !=
|
||||
-1;
|
||||
|
||||
void submitAction([_]) async {
|
||||
controller.text = controller.text.replaceAll('@', '').trim();
|
||||
if (controller.text.isEmpty) return;
|
||||
if (!formKey.currentState.validate()) return;
|
||||
final matrix = Matrix.of(context);
|
||||
|
||||
if ('@' + controller.text == matrix.client.userID) return;
|
||||
|
||||
final user = User(
|
||||
'@' + controller.text,
|
||||
room: Room(id: '', client: matrix.client),
|
||||
);
|
||||
final roomID = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => user.startDirectChat(),
|
||||
);
|
||||
|
||||
if (roomID.error == null) {
|
||||
await AdaptivePageLayout.of(context)
|
||||
.popAndPushNamed('/rooms/${roomID.result}');
|
||||
}
|
||||
}
|
||||
|
||||
void searchUserWithCoolDown([_]) async {
|
||||
coolDown?.cancel();
|
||||
coolDown = Timer(
|
||||
Duration(milliseconds: 500),
|
||||
() => searchUser(controller.text),
|
||||
);
|
||||
}
|
||||
|
||||
void searchUser(String text) async {
|
||||
if (text.isEmpty) {
|
||||
setState(() {
|
||||
foundProfiles = [];
|
||||
});
|
||||
}
|
||||
currentSearchTerm = text;
|
||||
if (currentSearchTerm.isEmpty) return;
|
||||
if (loading) return;
|
||||
setState(() => loading = true);
|
||||
final matrix = Matrix.of(context);
|
||||
UserSearchResult response;
|
||||
try {
|
||||
response = await matrix.client.searchUser(text, limit: 10);
|
||||
} catch (_) {}
|
||||
setState(() => loading = false);
|
||||
if (response?.results?.isEmpty ?? true) return;
|
||||
setState(() {
|
||||
foundProfiles = List<Profile>.from(response.results);
|
||||
});
|
||||
}
|
||||
|
||||
String validateForm(String value) {
|
||||
if (value.isEmpty) {
|
||||
return L10n.of(context).pleaseEnterAMatrixIdentifier;
|
||||
}
|
||||
final matrix = Matrix.of(context);
|
||||
final mxid = '@' + controller.text.trim();
|
||||
if (mxid == matrix.client.userID) {
|
||||
return L10n.of(context).youCannotInviteYourself;
|
||||
}
|
||||
if (!mxid.contains('@')) {
|
||||
return L10n.of(context).makeSureTheIdentifierIsValid;
|
||||
}
|
||||
if (!mxid.contains(':')) {
|
||||
return L10n.of(context).makeSureTheIdentifierIsValid;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void inviteAction() => FluffyShare.share(
|
||||
L10n.of(context).inviteText(Matrix.of(context).client.userID,
|
||||
'https://matrix.to/#/${Matrix.of(context).client.userID}'),
|
||||
context,
|
||||
);
|
||||
|
||||
void pickUser(Profile foundProfile) => setState(
|
||||
() => controller.text =
|
||||
currentSearchTerm = foundProfile.userId.substring(1),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => NewPrivateChatUI(this);
|
||||
}
|
||||
71
lib/views/sign_up.dart
Normal file
71
lib/views/sign_up.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
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/ui/sign_up_ui.dart';
|
||||
|
||||
import 'package:fluffychat/views/widgets/matrix.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class SignUp extends StatefulWidget {
|
||||
@override
|
||||
SignUpController createState() => SignUpController();
|
||||
}
|
||||
|
||||
class SignUpController extends State<SignUp> {
|
||||
final TextEditingController usernameController = TextEditingController();
|
||||
String usernameError;
|
||||
bool loading = false;
|
||||
MatrixFile avatar;
|
||||
|
||||
void setAvatarAction() async {
|
||||
final file =
|
||||
await FilePickerCross.importFromStorage(type: FileTypeCross.image);
|
||||
if (file != null) {
|
||||
setState(
|
||||
() => avatar = MatrixFile(
|
||||
bytes: file.toUint8List(),
|
||||
name: file.fileName,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void resetAvatarAction() => setState(() => avatar = null);
|
||||
|
||||
void signUpAction([_]) async {
|
||||
final matrix = Matrix.of(context);
|
||||
if (usernameController.text.isEmpty) {
|
||||
setState(() => usernameError = L10n.of(context).pleaseChooseAUsername);
|
||||
} else {
|
||||
setState(() => usernameError = null);
|
||||
}
|
||||
|
||||
if (usernameController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
setState(() => loading = true);
|
||||
|
||||
final preferredUsername =
|
||||
usernameController.text.toLowerCase().trim().replaceAll(' ', '-');
|
||||
|
||||
try {
|
||||
await matrix.client.usernameAvailable(preferredUsername);
|
||||
} on MatrixException catch (exception) {
|
||||
setState(() => usernameError = exception.errorMessage);
|
||||
return setState(() => loading = false);
|
||||
} catch (exception) {
|
||||
setState(() => usernameError = exception.toString());
|
||||
return setState(() => loading = false);
|
||||
}
|
||||
setState(() => loading = false);
|
||||
await AdaptivePageLayout.of(context).pushNamed(
|
||||
'/signup/password/${Uri.encodeComponent(preferredUsername)}/${Uri.encodeComponent(usernameController.text)}',
|
||||
arguments: avatar,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SignUpUI(this);
|
||||
}
|
||||
131
lib/views/sign_up_password.dart
Normal file
131
lib/views/sign_up_password.dart
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/views/ui/sign_up_password_ui.dart';
|
||||
|
||||
import 'package:fluffychat/views/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../utils/platform_infos.dart';
|
||||
|
||||
class SignUpPassword extends StatefulWidget {
|
||||
final MatrixFile avatar;
|
||||
final String username;
|
||||
final String displayname;
|
||||
const SignUpPassword(this.username, {this.avatar, this.displayname});
|
||||
@override
|
||||
SignUpPasswordController createState() => SignUpPasswordController();
|
||||
}
|
||||
|
||||
class SignUpPasswordController extends State<SignUpPassword> {
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
String passwordError;
|
||||
String _lastAuthWebViewStage;
|
||||
bool loading = false;
|
||||
bool showPassword = true;
|
||||
|
||||
void toggleShowPassword() => setState(() => showPassword = !showPassword);
|
||||
|
||||
void signUpAction({AuthenticationData auth}) async {
|
||||
final matrix = Matrix.of(context);
|
||||
if (passwordController.text.isEmpty) {
|
||||
setState(() => passwordError = L10n.of(context).pleaseEnterYourPassword);
|
||||
} else {
|
||||
setState(() => passwordError = null);
|
||||
}
|
||||
|
||||
if (passwordController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setState(() => loading = true);
|
||||
final waitForLogin = matrix.client.onLoginStateChanged.stream.first;
|
||||
await matrix.client.register(
|
||||
username: widget.username,
|
||||
password: passwordController.text,
|
||||
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||
auth: auth,
|
||||
);
|
||||
await waitForLogin;
|
||||
} on MatrixException catch (exception) {
|
||||
if (exception.requireAdditionalAuthentication) {
|
||||
final stages = exception.authenticationFlows.first.stages;
|
||||
|
||||
final currentStage = exception.completedAuthenticationFlows == null
|
||||
? stages.first
|
||||
: stages.firstWhere((stage) =>
|
||||
!exception.completedAuthenticationFlows.contains(stage) ??
|
||||
true);
|
||||
|
||||
if (currentStage == 'm.login.dummy') {
|
||||
signUpAction(
|
||||
auth: AuthenticationData(
|
||||
type: currentStage,
|
||||
session: exception.session,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (_lastAuthWebViewStage == currentStage) {
|
||||
_lastAuthWebViewStage = null;
|
||||
setState(
|
||||
() => passwordError = L10n.of(context).oopsSomethingWentWrong);
|
||||
return setState(() => loading = false);
|
||||
}
|
||||
_lastAuthWebViewStage = currentStage;
|
||||
await launch(
|
||||
Matrix.of(context).client.homeserver.toString() +
|
||||
'/_matrix/client/r0/auth/$currentStage/fallback/web?session=${exception.session}',
|
||||
);
|
||||
if (OkCancelResult.ok ==
|
||||
await showOkCancelAlertDialog(
|
||||
message: L10n.of(context).pleaseFollowInstructionsOnWeb,
|
||||
context: context,
|
||||
okLabel: L10n.of(context).next,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
)) {
|
||||
signUpAction(
|
||||
auth: AuthenticationData(session: exception.session),
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
loading = false;
|
||||
passwordError = null;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setState(() => passwordError = exception.errorMessage);
|
||||
return setState(() => loading = false);
|
||||
}
|
||||
} catch (exception) {
|
||||
setState(() => passwordError = exception.toString());
|
||||
return setState(() => loading = false);
|
||||
}
|
||||
await matrix.client.onLoginStateChanged.stream
|
||||
.firstWhere((l) => l == LoginState.logged);
|
||||
try {
|
||||
await matrix.client
|
||||
.setDisplayname(matrix.client.userID, widget.displayname);
|
||||
} catch (exception) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context).couldNotSetDisplayname)));
|
||||
}
|
||||
if (widget.avatar != null) {
|
||||
try {
|
||||
await matrix.client.setAvatar(widget.avatar);
|
||||
} catch (exception) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context).couldNotSetAvatar)));
|
||||
}
|
||||
}
|
||||
if (mounted) setState(() => loading = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SignUpPasswordUI(this);
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
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:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ArchiveView extends StatelessWidget {
|
||||
class ArchiveUI extends StatelessWidget {
|
||||
final ArchiveController controller;
|
||||
|
||||
const ArchiveView(this.controller, {Key key}) : super(key: key);
|
||||
const ArchiveUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
369
lib/views/ui/chat_details_ui.dart
Normal file
369
lib/views/ui/chat_details_ui.dart
Normal file
|
|
@ -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: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/matrix.dart';
|
||||
import 'package:fluffychat/views/widgets/max_width_body.dart';
|
||||
import 'package:flutter/material.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;
|
||||
|
||||
const ChatEncryptionSettingsView(this.controller, {Key key})
|
||||
: super(key: key);
|
||||
const ChatEncryptionSettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.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/list_items/chat_list_item.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'widgets/matrix.dart';
|
||||
import '../widgets/matrix.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ChatListView extends StatelessWidget {
|
||||
class ChatListUI extends StatelessWidget {
|
||||
final ChatListController controller;
|
||||
|
||||
const ChatListView(this.controller, {Key key}) : super(key: key);
|
||||
const ChatListUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
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/max_width_body.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:famedlysdk/famedlysdk.dart';
|
||||
|
||||
class ChatPermissionsSettingsView extends StatelessWidget {
|
||||
class ChatPermissionsSettingsUI extends StatelessWidget {
|
||||
final ChatPermissionsSettingsController controller;
|
||||
|
||||
const ChatPermissionsSettingsView(this.controller, {Key key})
|
||||
: super(key: key);
|
||||
const ChatPermissionsSettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
747
lib/views/ui/chat_ui.dart
Normal file
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:flutter/material.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;
|
||||
|
||||
const DevicesSettingsView(this.controller, {Key key}) : super(key: key);
|
||||
const DevicesSettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
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/fluffy_banner.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:url_launcher/url_launcher.dart';
|
||||
|
||||
class HomeserverPickerView extends StatelessWidget {
|
||||
class HomeserverPickerUI extends StatelessWidget {
|
||||
final HomeserverPickerController controller;
|
||||
|
||||
const HomeserverPickerView(
|
||||
this.controller, {
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
const HomeserverPickerUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
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:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ImageViewerView extends StatelessWidget {
|
||||
class ImageViewerUI extends StatelessWidget {
|
||||
final ImageViewerController controller;
|
||||
|
||||
const ImageViewerView(this.controller, {Key key}) : super(key: key);
|
||||
const ImageViewerUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
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:famedlysdk/famedlysdk.dart';
|
||||
|
|
@ -8,13 +8,10 @@ import 'package:fluffychat/views/widgets/matrix.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class InvitationSelectionView extends StatelessWidget {
|
||||
class InvitationSelectionUI extends StatelessWidget {
|
||||
final InvitationSelectionController controller;
|
||||
|
||||
const InvitationSelectionView(
|
||||
this.controller, {
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
const InvitationSelectionUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -9,7 +9,7 @@ import 'package:fluffychat/views/widgets/matrix.dart';
|
|||
|
||||
import 'package:flutter/material.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';
|
||||
|
||||
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:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class NewGroupView extends StatelessWidget {
|
||||
class NewGroupUI extends StatelessWidget {
|
||||
final NewGroupController controller;
|
||||
|
||||
const NewGroupView(
|
||||
this.controller, {
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
const NewGroupUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
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/contacts_list.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:famedlysdk/famedlysdk.dart';
|
||||
|
||||
class NewPrivateChatView extends StatelessWidget {
|
||||
class NewPrivateChatUI extends StatelessWidget {
|
||||
final NewPrivateChatController controller;
|
||||
|
||||
const NewPrivateChatView(this.controller, {Key key}) : super(key: key);
|
||||
const NewPrivateChatUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -11,7 +11,7 @@ import 'package:fluffychat/views/widgets/matrix.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.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 {
|
||||
final String alias;
|
||||
|
|
@ -13,7 +13,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import '../views/widgets/matrix.dart';
|
||||
import '../widgets/matrix.dart';
|
||||
|
||||
class EmotesSettings extends StatefulWidget {
|
||||
final Room room;
|
||||
|
|
@ -5,7 +5,7 @@ import 'package:future_loading_dialog/future_loading_dialog.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../views/widgets/matrix.dart';
|
||||
import '../widgets/matrix.dart';
|
||||
|
||||
class SettingsIgnoreList extends StatefulWidget {
|
||||
final String initialUserId;
|
||||
|
|
@ -9,9 +9,9 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.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 {
|
||||
final PushRuleKind type;
|
||||
|
|
@ -7,8 +7,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../config/app_config.dart';
|
||||
import '../views/widgets/matrix.dart';
|
||||
import '../../config/app_config.dart';
|
||||
import '../widgets/matrix.dart';
|
||||
|
||||
class SettingsStyle extends StatefulWidget {
|
||||
@override
|
||||
|
|
@ -21,11 +21,11 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||
import 'package:image_picker/image_picker.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 '../views/widgets/matrix.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../config/setting_keys.dart';
|
||||
import '../widgets/matrix.dart';
|
||||
import '../../config/app_config.dart';
|
||||
import '../../config/setting_keys.dart';
|
||||
|
||||
class Settings extends StatefulWidget {
|
||||
@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:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class SignUpPasswordView extends StatelessWidget {
|
||||
class SignUpPasswordUI extends StatelessWidget {
|
||||
final SignUpPasswordController controller;
|
||||
|
||||
const SignUpPasswordView(
|
||||
this.controller, {
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
const SignUpPasswordUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
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/matrix.dart';
|
||||
|
|
@ -8,13 +8,10 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class SignUpView extends StatelessWidget {
|
||||
class SignUpUI extends StatelessWidget {
|
||||
final SignUpController controller;
|
||||
|
||||
const SignUpView(
|
||||
this.controller, {
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
const SignUpUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
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/foundation.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:http/http.dart' as http;
|
||||
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:dbus/dbus.dart';
|
||||
import 'package:desktop_notifications/desktop_notifications.dart';*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue