refactor: Rename views to pages
This commit is contained in:
parent
c8827ae1b2
commit
74606b1600
63 changed files with 77 additions and 77 deletions
59
lib/pages/views/archive_view.dart
Normal file
59
lib/pages/views/archive_view.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/archive.dart';
|
||||
import 'package:fluffychat/widgets/list_items/chat_list_item.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ArchiveView extends StatelessWidget {
|
||||
final ArchiveController controller;
|
||||
|
||||
const ArchiveView(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<Room>>(
|
||||
future: controller.getArchive(context),
|
||||
builder: (BuildContext context, snapshot) => Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).archive),
|
||||
actions: [
|
||||
if (snapshot.hasData &&
|
||||
controller.archive != null &&
|
||||
controller.archive.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: controller.forgetAllAction,
|
||||
child: Text(L10n.of(context).clearArchive),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
L10n.of(context).oopsSomethingWentWrong,
|
||||
textAlign: TextAlign.center,
|
||||
));
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
} else {
|
||||
controller.archive = snapshot.data;
|
||||
if (controller.archive.isEmpty) {
|
||||
return Center(child: Icon(Icons.archive_outlined, size: 80));
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: controller.archive.length,
|
||||
itemBuilder: (BuildContext context, int i) => ChatListItem(
|
||||
controller.archive[i],
|
||||
onForget: controller.forgetAction,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
367
lib/pages/views/chat_details_ui.dart
Normal file
367
lib/pages/views/chat_details_ui.dart
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat_details.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
||||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
||||
import 'package:fluffychat/widgets/content_banner.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/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.joinRules == JoinRules.public)
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(Icons.link_outlined),
|
||||
),
|
||||
onTap: controller.editAliases,
|
||||
title: Text(L10n.of(context).editRoomAliases),
|
||||
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.worldReadable,
|
||||
child: Text(HistoryVisibility.worldReadable
|
||||
.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.canJoin,
|
||||
child: Text(
|
||||
GuestAccess.canJoin.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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
199
lib/pages/views/chat_encryption_settings_ui.dart
Normal file
199
lib/pages/views/chat_encryption_settings_ui.dart
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/chat_encryption_settings.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import '../../utils/device_extension.dart';
|
||||
|
||||
class ChatEncryptionSettingsUI extends StatelessWidget {
|
||||
final ChatEncryptionSettingsController controller;
|
||||
|
||||
const ChatEncryptionSettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room = Matrix.of(context).client.getRoomById(controller.widget.id);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).tapOnDeviceToVerify),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(56),
|
||||
child: ListTile(
|
||||
title: Text(L10n.of(context).deviceVerifyDescription),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).secondaryHeaderColor,
|
||||
foregroundColor: Theme.of(context).accentColor,
|
||||
child: Icon(Icons.lock),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
return FutureBuilder<List<DeviceKeys>>(
|
||||
future: room.getUserDeviceKeys(),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(L10n.of(context).oopsSomethingWentWrong +
|
||||
': ' +
|
||||
snapshot.error.toString()),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final deviceKeys = snapshot.data;
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: deviceKeys.length,
|
||||
itemBuilder: (BuildContext context, int i) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (i == 0 ||
|
||||
deviceKeys[i].userId !=
|
||||
deviceKeys[i - 1].userId) ...{
|
||||
Divider(height: 1, thickness: 1),
|
||||
PopupMenuButton(
|
||||
onSelected: (action) => controller.onSelected(
|
||||
context, action, deviceKeys[i]),
|
||||
itemBuilder: (c) {
|
||||
final items = <PopupMenuEntry<String>>[];
|
||||
if (room
|
||||
.client
|
||||
.userDeviceKeys[deviceKeys[i].userId]
|
||||
.verified ==
|
||||
UserVerifiedStatus.unknown) {
|
||||
items.add(PopupMenuItem(
|
||||
value: 'verify_user',
|
||||
child: Text(L10n.of(context).verifyUser),
|
||||
));
|
||||
}
|
||||
return items;
|
||||
},
|
||||
child: ListTile(
|
||||
leading: Avatar(
|
||||
room
|
||||
.getUserByMXIDSync(deviceKeys[i].userId)
|
||||
.avatarUrl,
|
||||
room
|
||||
.getUserByMXIDSync(deviceKeys[i].userId)
|
||||
.calcDisplayname(),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
room
|
||||
.getUserByMXIDSync(deviceKeys[i].userId)
|
||||
.calcDisplayname(),
|
||||
),
|
||||
Spacer(),
|
||||
Text(
|
||||
deviceKeys[i].userId,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
.color
|
||||
.withAlpha(150),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
PopupMenuButton(
|
||||
onSelected: (action) => controller.onSelected(
|
||||
context, action, deviceKeys[i]),
|
||||
itemBuilder: (c) {
|
||||
final items = <PopupMenuEntry<String>>[];
|
||||
if (deviceKeys[i].blocked ||
|
||||
!deviceKeys[i].verified) {
|
||||
items.add(PopupMenuItem(
|
||||
value:
|
||||
deviceKeys[i].userId == room.client.userID
|
||||
? 'verify'
|
||||
: 'verify_user',
|
||||
child: Text(L10n.of(context).verifyStart),
|
||||
));
|
||||
}
|
||||
if (deviceKeys[i].blocked) {
|
||||
items.add(PopupMenuItem(
|
||||
value: 'unblock',
|
||||
child: Text(L10n.of(context).unblockDevice),
|
||||
));
|
||||
}
|
||||
if (!deviceKeys[i].blocked) {
|
||||
items.add(PopupMenuItem(
|
||||
value: 'block',
|
||||
child: Text(L10n.of(context).blockDevice),
|
||||
));
|
||||
}
|
||||
return items;
|
||||
},
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
foregroundColor:
|
||||
Theme.of(context).textTheme.bodyText1.color,
|
||||
backgroundColor:
|
||||
Theme.of(context).secondaryHeaderColor,
|
||||
child: Icon(deviceKeys[i].icon),
|
||||
),
|
||||
title: Text(
|
||||
deviceKeys[i].displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
Text(
|
||||
deviceKeys[i].deviceId,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
.color
|
||||
.withAlpha(150),
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Text(
|
||||
deviceKeys[i].blocked
|
||||
? L10n.of(context).blocked
|
||||
: deviceKeys[i].verified
|
||||
? L10n.of(context).verified
|
||||
: L10n.of(context).unknownDevice,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: deviceKeys[i].blocked
|
||||
? Colors.red
|
||||
: deviceKeys[i].verified
|
||||
? Colors.green
|
||||
: Colors.orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
242
lib/pages/views/chat_list_ui.dart
Normal file
242
lib/pages/views/chat_list_ui.dart
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/chat_list.dart';
|
||||
import 'package:fluffychat/widgets/connection_status_header.dart';
|
||||
import 'package:fluffychat/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 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ChatListUI extends StatelessWidget {
|
||||
final ChatListController controller;
|
||||
|
||||
const ChatListUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<Object>(
|
||||
stream: Matrix.of(context).onShareContentChanged.stream,
|
||||
builder: (_, __) {
|
||||
final selectMode = Matrix.of(context).shareContent != null
|
||||
? SelectMode.share
|
||||
: controller.selectedRoomIds.isEmpty
|
||||
? SelectMode.normal
|
||||
: SelectMode.select;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation:
|
||||
AdaptivePageLayout.of(context).columnMode(context) ? 1 : null,
|
||||
leading: selectMode == SelectMode.normal
|
||||
? null
|
||||
: IconButton(
|
||||
tooltip: L10n.of(context).cancel,
|
||||
icon: Icon(Icons.close_outlined),
|
||||
onPressed: controller.cancelAction,
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: selectMode == SelectMode.share
|
||||
? null
|
||||
: selectMode == SelectMode.select
|
||||
? [
|
||||
if (controller.selectedRoomIds.length == 1)
|
||||
IconButton(
|
||||
tooltip: L10n.of(context).toggleUnread,
|
||||
icon: Icon(Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(
|
||||
controller.selectedRoomIds.single)
|
||||
.isUnread
|
||||
? Icons.mark_chat_read_outlined
|
||||
: Icons.mark_chat_unread_outlined),
|
||||
onPressed: controller.toggleUnread,
|
||||
),
|
||||
if (controller.selectedRoomIds.length == 1)
|
||||
IconButton(
|
||||
tooltip: L10n.of(context).toggleFavorite,
|
||||
icon: Icon(Icons.push_pin_outlined),
|
||||
onPressed: controller.toggleFavouriteRoom,
|
||||
),
|
||||
if (controller.selectedRoomIds.length == 1)
|
||||
IconButton(
|
||||
icon: Icon(Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(
|
||||
controller.selectedRoomIds.single)
|
||||
.pushRuleState ==
|
||||
PushRuleState.notify
|
||||
? Icons.notifications_off_outlined
|
||||
: Icons.notifications_outlined),
|
||||
tooltip: L10n.of(context).toggleMuted,
|
||||
onPressed: controller.toggleMuted,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.archive_outlined),
|
||||
tooltip: L10n.of(context).archive,
|
||||
onPressed: controller.archiveAction,
|
||||
),
|
||||
]
|
||||
: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.search_outlined),
|
||||
tooltip: L10n.of(context).search,
|
||||
onPressed: () => AdaptivePageLayout.of(context)
|
||||
.pushNamed('/search'),
|
||||
),
|
||||
PopupMenuButton<PopupMenuAction>(
|
||||
onSelected: controller.onPopupMenuSelect,
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.setStatus,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.edit_outlined),
|
||||
SizedBox(width: 12),
|
||||
Text(L10n.of(context).setStatus),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.newGroup,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.group_add_outlined),
|
||||
SizedBox(width: 12),
|
||||
Text(L10n.of(context).createNewGroup),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.invite,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.share_outlined),
|
||||
SizedBox(width: 12),
|
||||
Text(L10n.of(context).inviteContact),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.archive,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.archive_outlined),
|
||||
SizedBox(width: 12),
|
||||
Text(L10n.of(context).archive),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.settings,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.settings_outlined),
|
||||
SizedBox(width: 12),
|
||||
Text(L10n.of(context).settings),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
title: Text(selectMode == SelectMode.share
|
||||
? L10n.of(context).share
|
||||
: selectMode == SelectMode.select
|
||||
? L10n.of(context).numberSelected(
|
||||
controller.selectedRoomIds.length.toString())
|
||||
: AppConfig.applicationName),
|
||||
),
|
||||
body: Column(children: [
|
||||
ConnectionStatusHeader(),
|
||||
Expanded(
|
||||
child: StreamBuilder(
|
||||
stream: Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where((s) => s.hasRoomUpdate),
|
||||
builder: (context, snapshot) {
|
||||
return FutureBuilder<void>(
|
||||
future: controller.waitForFirstSync(),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final rooms = List<Room>.from(
|
||||
Matrix.of(context).client.rooms);
|
||||
rooms.removeWhere((room) => room.lastEvent == null);
|
||||
if (rooms.isEmpty) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.maps_ugc_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
L10n.of(context).startYourFirstChat,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
final totalCount = rooms.length;
|
||||
return ListView.builder(
|
||||
itemCount: totalCount,
|
||||
itemBuilder: (BuildContext context, int i) =>
|
||||
ChatListItem(
|
||||
rooms[i],
|
||||
selected: controller.selectedRoomIds
|
||||
.contains(rooms[i].id),
|
||||
onTap: selectMode == SelectMode.select
|
||||
? () =>
|
||||
controller.toggleSelection(rooms[i].id)
|
||||
: null,
|
||||
onLongPress: () =>
|
||||
controller.toggleSelection(rooms[i].id),
|
||||
activeChat:
|
||||
controller.widget.activeChat == rooms[i].id,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
]),
|
||||
floatingActionButton: selectMode == SelectMode.normal
|
||||
? FloatingActionButton(
|
||||
onPressed: () => AdaptivePageLayout.of(context)
|
||||
.pushNamedAndRemoveUntilIsFirst('/newprivatechat'),
|
||||
child: Icon(CupertinoIcons.chat_bubble),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatListPopupMenuItemActions {
|
||||
createGroup,
|
||||
discover,
|
||||
setStatus,
|
||||
inviteContact,
|
||||
settings,
|
||||
}
|
||||
124
lib/pages/views/chat_permissions_settings_ui.dart
Normal file
124
lib/pages/views/chat_permissions_settings_ui.dart
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import 'package:fluffychat/pages/chat_permissions_settings.dart';
|
||||
import 'package:fluffychat/widgets/list_items/permission_list_tile.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
||||
class ChatPermissionsSettingsUI extends StatelessWidget {
|
||||
final ChatPermissionsSettingsController controller;
|
||||
|
||||
const ChatPermissionsSettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).editChatPermissions),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: StreamBuilder(
|
||||
stream: controller.onChanged,
|
||||
builder: (context, _) {
|
||||
final room =
|
||||
Matrix.of(context).client.getRoomById(controller.widget.roomId);
|
||||
final powerLevelsContent = Map<String, dynamic>.from(
|
||||
room.getState(EventTypes.RoomPowerLevels).content);
|
||||
final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
|
||||
..removeWhere((k, v) => !(v is int));
|
||||
final eventsPowerLevels =
|
||||
Map<String, dynamic>.from(powerLevelsContent['events'] ?? {})
|
||||
..removeWhere((k, v) => !(v is int));
|
||||
return Column(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var entry in powerLevels.entries)
|
||||
PermissionsListTile(
|
||||
permissionKey: entry.key,
|
||||
permission: entry.value,
|
||||
onTap: () => controller.editPowerLevel(
|
||||
context, entry.key, entry.value),
|
||||
),
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).notifications,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
final key = 'rooms';
|
||||
final int value = powerLevelsContent
|
||||
.containsKey('notifications')
|
||||
? powerLevelsContent['notifications']['rooms'] ?? 0
|
||||
: 0;
|
||||
return PermissionsListTile(
|
||||
permissionKey: key,
|
||||
permission: value,
|
||||
category: 'notifications',
|
||||
onTap: () => controller.editPowerLevel(
|
||||
context, key, value,
|
||||
category: 'notifications'),
|
||||
);
|
||||
}),
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).configureChat,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (eventsPowerLevels != null)
|
||||
for (var entry in eventsPowerLevels.entries)
|
||||
PermissionsListTile(
|
||||
permissionKey: entry.key,
|
||||
category: 'events',
|
||||
permission: entry.value,
|
||||
onTap: () => controller.editPowerLevel(
|
||||
context, entry.key, entry.value,
|
||||
category: 'events'),
|
||||
),
|
||||
if (room.canSendEvent(EventTypes.RoomTombstone)) ...{
|
||||
Divider(thickness: 1),
|
||||
FutureBuilder<ServerCapabilities>(
|
||||
future: room.client.getCapabilities(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final String roomVersion = room
|
||||
.getState(EventTypes.RoomCreate)
|
||||
.content['room_version'] ??
|
||||
'1';
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'${L10n.of(context).roomVersion}: $roomVersion'),
|
||||
onTap: () =>
|
||||
controller.updateRoomAction(snapshot.data),
|
||||
);
|
||||
},
|
||||
),
|
||||
},
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
756
lib/pages/views/chat_ui.dart
Normal file
756
lib/pages/views/chat_ui.dart
Normal file
|
|
@ -0,0 +1,756 @@
|
|||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/chat.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
||||
import 'package:fluffychat/widgets/connection_status_header.dart';
|
||||
import 'package:fluffychat/widgets/input_bar.dart';
|
||||
import 'package:fluffychat/widgets/unread_badge_back_button.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/encryption_button.dart';
|
||||
import 'package:fluffychat/widgets/list_items/message.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/reply_content.dart';
|
||||
import 'package:fluffychat/pages/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),
|
||||
outerContext: context,
|
||||
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 (controller.timeline == null) {
|
||||
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,
|
||||
bottom: 4,
|
||||
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,
|
||||
outerContext: context,
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!controller.showEmojiPicker)
|
||||
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,
|
||||
),
|
||||
if (controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership == Membership.join &&
|
||||
!controller.showEmojiPicker)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
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>[
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: 200),
|
||||
height: 56,
|
||||
width: controller.inputText.isEmpty ? 56 : 0,
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
height: controller.showEmojiPicker
|
||||
? MediaQuery.of(context).size.height / 2
|
||||
: 0,
|
||||
child: controller.showEmojiPicker
|
||||
? EmojiPicker(
|
||||
onEmojiSelected: controller.onEmojiSelected,
|
||||
onBackspacePressed: controller.cancelEmojiPicker,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
97
lib/pages/views/device_settings_ui.dart
Normal file
97
lib/pages/views/device_settings_ui.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import 'package:fluffychat/pages/device_settings.dart';
|
||||
import 'package:fluffychat/widgets/layouts/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';
|
||||
|
||||
class DevicesSettingsUI extends StatelessWidget {
|
||||
final DevicesSettingsController controller;
|
||||
|
||||
const DevicesSettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).devices),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: FutureBuilder<bool>(
|
||||
future: controller.loadUserDevices(context),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(Icons.error_outlined),
|
||||
Text(snapshot.error.toString()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData || controller.devices == null) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (controller.thisDevice != null)
|
||||
UserDeviceListItem(
|
||||
controller.thisDevice,
|
||||
rename: controller.renameDeviceAction,
|
||||
remove: (d) => controller.removeDevicesAction([d]),
|
||||
verify: controller.verifyDeviceAction,
|
||||
block: controller.blockDeviceAction,
|
||||
unblock: controller.unblockDeviceAction,
|
||||
),
|
||||
Divider(height: 1),
|
||||
if (controller.notThisDevice.isNotEmpty)
|
||||
ListTile(
|
||||
title: Text(
|
||||
controller.errorDeletingDevices ??
|
||||
L10n.of(context).removeAllOtherDevices,
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
trailing: controller.loadingDeletingDevices
|
||||
? CircularProgressIndicator()
|
||||
: Icon(Icons.delete_outline),
|
||||
onTap: controller.loadingDeletingDevices
|
||||
? null
|
||||
: () => controller
|
||||
.removeDevicesAction(controller.notThisDevice),
|
||||
),
|
||||
Divider(height: 1),
|
||||
Expanded(
|
||||
child: controller.notThisDevice.isEmpty
|
||||
? Center(
|
||||
child: Icon(
|
||||
Icons.devices_other,
|
||||
size: 60,
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
separatorBuilder: (BuildContext context, int i) =>
|
||||
Divider(height: 1),
|
||||
itemCount: controller.notThisDevice.length,
|
||||
itemBuilder: (BuildContext context, int i) =>
|
||||
UserDeviceListItem(
|
||||
controller.notThisDevice[i],
|
||||
rename: controller.renameDeviceAction,
|
||||
remove: (d) => controller.removeDevicesAction([d]),
|
||||
verify: controller.verifyDeviceAction,
|
||||
block: controller.blockDeviceAction,
|
||||
unblock: controller.unblockDeviceAction,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
12
lib/pages/views/empty_page_ui.dart
Normal file
12
lib/pages/views/empty_page_ui.dart
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmptyPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Image.asset('assets/favicon.png', width: 100, height: 100),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
109
lib/pages/views/homeserver_picker_ui.dart
Normal file
109
lib/pages/views/homeserver_picker_ui.dart
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import '../homeserver_picker.dart';
|
||||
import 'package:fluffychat/widgets/default_app_bar_search_field.dart';
|
||||
import 'package:fluffychat/widgets/fluffy_banner.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/widgets/layouts/one_page_card.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:url_launcher/url_launcher.dart';
|
||||
|
||||
class HomeserverPickerUI extends StatelessWidget {
|
||||
final HomeserverPickerController controller;
|
||||
|
||||
const HomeserverPickerUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OnePageCard(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 8,
|
||||
title: DefaultAppBarSearchField(
|
||||
prefixText: 'https://',
|
||||
hintText: L10n.of(context).enterYourHomeserver,
|
||||
searchController: controller.homeserverController,
|
||||
suffix: Icon(Icons.edit_outlined),
|
||||
padding: EdgeInsets.zero,
|
||||
onChanged: (s) => controller.domain = s,
|
||||
readOnly: !AppConfig.allowOtherHomeservers,
|
||||
onSubmit: (_) => controller.checkHomeserverAction(),
|
||||
unfocusOnClear: false,
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
Hero(
|
||||
tag: 'loginBanner',
|
||||
child: FluffyBanner(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
AppConfig.applicationWelcomeMessage ??
|
||||
L10n.of(context).welcomeText,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Hero(
|
||||
tag: 'loginButton',
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.isLoading
|
||||
? null
|
||||
: controller.checkHomeserverAction,
|
||||
child: controller.isLoading
|
||||
? LinearProgressIndicator()
|
||||
: Text(
|
||||
L10n.of(context).connect.toUpperCase(),
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => launch(AppConfig.privacyUrl),
|
||||
child: Text(
|
||||
L10n.of(context).privacy,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.blueGrey,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => PlatformInfos.showDialog(context),
|
||||
child: Text(
|
||||
L10n.of(context).about,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.blueGrey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
59
lib/pages/views/image_viewer_ui.dart
Normal file
59
lib/pages/views/image_viewer_ui.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import '../image_viewer.dart';
|
||||
import 'package:fluffychat/widgets/image_bubble.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ImageViewerUI extends StatelessWidget {
|
||||
final ImageViewerController controller;
|
||||
|
||||
const ImageViewerUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: Navigator.of(context, rootNavigator: false).pop,
|
||||
color: Colors.white,
|
||||
tooltip: L10n.of(context).close,
|
||||
),
|
||||
backgroundColor: Color(0x44000000),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.reply_outlined),
|
||||
onPressed: controller.forwardAction,
|
||||
color: Colors.white,
|
||||
tooltip: L10n.of(context).share,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.download_outlined),
|
||||
onPressed: controller.openFileAction,
|
||||
color: Colors.white,
|
||||
tooltip: L10n.of(context).downloadFile,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: InteractiveViewer(
|
||||
minScale: 1.0,
|
||||
maxScale: 10.0,
|
||||
onInteractionEnd: controller.onInteractionEnds,
|
||||
child: Center(
|
||||
child: ImageBubble(
|
||||
controller.widget.event,
|
||||
tapToView: false,
|
||||
onLoaded: controller.widget.onLoaded,
|
||||
fit: BoxFit.contain,
|
||||
backgroundColor: Colors.black,
|
||||
maxSize: false,
|
||||
radius: 0.0,
|
||||
thumbnailOnly: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
83
lib/pages/views/invitation_selection_ui.dart
Normal file
83
lib/pages/views/invitation_selection_ui.dart
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import 'package:fluffychat/pages/invitation_selection.dart';
|
||||
import 'package:fluffychat/widgets/default_app_bar_search_field.dart';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class InvitationSelectionUI extends StatelessWidget {
|
||||
final InvitationSelectionController controller;
|
||||
|
||||
const InvitationSelectionUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room =
|
||||
Matrix.of(context).client.getRoomById(controller.widget.roomId);
|
||||
final groupName =
|
||||
room.name?.isEmpty ?? false ? L10n.of(context).group : room.name;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
titleSpacing: 0,
|
||||
title: DefaultAppBarSearchField(
|
||||
autofocus: true,
|
||||
hintText: L10n.of(context).inviteContactToGroup(groupName),
|
||||
onChanged: controller.searchUserWithCoolDown,
|
||||
),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: controller.foundProfiles.isNotEmpty
|
||||
? ListView.builder(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: controller.foundProfiles.length,
|
||||
itemBuilder: (BuildContext context, int i) => ListTile(
|
||||
leading: Avatar(
|
||||
controller.foundProfiles[i].avatarUrl,
|
||||
controller.foundProfiles[i].displayname ??
|
||||
controller.foundProfiles[i].userId,
|
||||
),
|
||||
title: Text(
|
||||
controller.foundProfiles[i].displayname ??
|
||||
controller.foundProfiles[i].userId.localpart,
|
||||
),
|
||||
subtitle: Text(controller.foundProfiles[i].userId),
|
||||
onTap: () => controller.inviteAction(
|
||||
context, controller.foundProfiles[i].userId),
|
||||
),
|
||||
)
|
||||
: FutureBuilder<List<User>>(
|
||||
future: controller.getContacts(context),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
final contacts = snapshot.data;
|
||||
return ListView.builder(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: contacts.length,
|
||||
itemBuilder: (BuildContext context, int i) => ListTile(
|
||||
leading: Avatar(
|
||||
contacts[i].avatarUrl,
|
||||
contacts[i].calcDisplayname(),
|
||||
),
|
||||
title: Text(contacts[i].calcDisplayname()),
|
||||
subtitle: Text(contacts[i].id),
|
||||
onTap: () =>
|
||||
controller.inviteAction(context, contacts[i].id),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
285
lib/pages/views/login_ui.dart
Normal file
285
lib/pages/views/login_ui.dart
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/widgets/layouts/one_page_card.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import '../../utils/platform_infos.dart';
|
||||
import 'package:email_validator/email_validator.dart';
|
||||
|
||||
class Login extends StatefulWidget {
|
||||
@override
|
||||
_LoginState createState() => _LoginState();
|
||||
}
|
||||
|
||||
class _LoginState extends State<Login> {
|
||||
final TextEditingController usernameController = TextEditingController();
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
String usernameError;
|
||||
String passwordError;
|
||||
bool loading = false;
|
||||
bool showPassword = false;
|
||||
|
||||
void login(BuildContext context) async {
|
||||
final matrix = Matrix.of(context);
|
||||
if (usernameController.text.isEmpty) {
|
||||
setState(() => usernameError = L10n.of(context).pleaseEnterYourUsername);
|
||||
} else {
|
||||
setState(() => usernameError = null);
|
||||
}
|
||||
if (passwordController.text.isEmpty) {
|
||||
setState(() => passwordError = L10n.of(context).pleaseEnterYourPassword);
|
||||
} else {
|
||||
setState(() => passwordError = null);
|
||||
}
|
||||
|
||||
if (usernameController.text.isEmpty || passwordController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => loading = true);
|
||||
try {
|
||||
final username = usernameController.text;
|
||||
AuthenticationIdentifier identifier;
|
||||
if (username.isEmail) {
|
||||
identifier = AuthenticationThirdPartyIdentifier(
|
||||
medium: 'email',
|
||||
address: username,
|
||||
);
|
||||
} else if (username.isPhoneNumber) {
|
||||
identifier = AuthenticationThirdPartyIdentifier(
|
||||
medium: 'msisdn',
|
||||
address: username,
|
||||
);
|
||||
} else {
|
||||
identifier = AuthenticationUserIdentifier(user: username);
|
||||
}
|
||||
await matrix.client.login(
|
||||
identifier: identifier,
|
||||
// To stay compatible with older server versions
|
||||
// ignore: deprecated_member_use
|
||||
user: identifier.type == AuthenticationIdentifierTypes.userId
|
||||
? username
|
||||
: null,
|
||||
password: passwordController.text,
|
||||
initialDeviceDisplayName: PlatformInfos.clientName);
|
||||
} on MatrixException catch (exception) {
|
||||
setState(() => passwordError = exception.errorMessage);
|
||||
return setState(() => loading = false);
|
||||
} catch (exception) {
|
||||
setState(() => passwordError = exception.toString());
|
||||
return setState(() => loading = false);
|
||||
}
|
||||
|
||||
if (mounted) setState(() => loading = false);
|
||||
}
|
||||
|
||||
Timer _coolDown;
|
||||
|
||||
void _checkWellKnownWithCoolDown(String userId, BuildContext context) async {
|
||||
_coolDown?.cancel();
|
||||
_coolDown = Timer(
|
||||
Duration(seconds: 1),
|
||||
() => _checkWellKnown(userId, context),
|
||||
);
|
||||
}
|
||||
|
||||
void _checkWellKnown(String userId, BuildContext context) async {
|
||||
setState(() => usernameError = null);
|
||||
if (!userId.isValidMatrixId) return;
|
||||
try {
|
||||
final wellKnownInformations = await Matrix.of(context)
|
||||
.client
|
||||
.getWellKnownInformationsByUserId(userId);
|
||||
final newDomain = wellKnownInformations.mHomeserver?.baseUrl;
|
||||
if ((newDomain?.isNotEmpty ?? false) &&
|
||||
newDomain != Matrix.of(context).client.homeserver.toString()) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.checkHomeserver(newDomain),
|
||||
);
|
||||
setState(() => usernameError = null);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => usernameError = e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _passwordForgotten(BuildContext context) async {
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).enterAnEmailAddress,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context).enterAnEmailAddress,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (input == null) return;
|
||||
final clientSecret = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final response = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.resetPasswordUsingEmail(
|
||||
input.single,
|
||||
clientSecret,
|
||||
sendAttempt++,
|
||||
),
|
||||
);
|
||||
if (response.error != null) return;
|
||||
final ok = await showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).weSentYouAnEmail,
|
||||
message: L10n.of(context).pleaseClickOnLink,
|
||||
okLabel: L10n.of(context).iHaveClickedOnLink,
|
||||
useRootNavigator: false,
|
||||
);
|
||||
if (ok == null) return;
|
||||
final password = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).chooseAStrongPassword,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: '******',
|
||||
obscureText: true,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (password == null) return;
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.changePassword(
|
||||
password.single,
|
||||
auth: AuthenticationThreePidCreds(
|
||||
type: AuthenticationTypes.emailIdentity,
|
||||
threepidCreds: [
|
||||
ThreepidCreds(
|
||||
sid: (response as RequestTokenResponse).sid,
|
||||
clientSecret: clientSecret,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (success.error == null) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context).passwordHasBeenChanged)));
|
||||
}
|
||||
}
|
||||
|
||||
static int sendAttempt = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OnePageCard(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: loading ? Container() : BackButton(),
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
L10n.of(context).logInTo(Matrix.of(context)
|
||||
.client
|
||||
.homeserver
|
||||
.toString()
|
||||
.replaceFirst('https://', '')),
|
||||
),
|
||||
),
|
||||
body: Builder(builder: (context) {
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
readOnly: loading,
|
||||
autocorrect: false,
|
||||
autofocus: true,
|
||||
onChanged: (t) => _checkWellKnownWithCoolDown(t, context),
|
||||
controller: usernameController,
|
||||
autofillHints: loading ? null : [AutofillHints.username],
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(Icons.account_box_outlined),
|
||||
hintText:
|
||||
'@${L10n.of(context).username.toLowerCase()}:domain',
|
||||
errorText: usernameError,
|
||||
labelText: L10n.of(context).username),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
readOnly: loading,
|
||||
autocorrect: false,
|
||||
autofillHints: loading ? null : [AutofillHints.password],
|
||||
controller: passwordController,
|
||||
obscureText: !showPassword,
|
||||
onSubmitted: (t) => login(context),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(Icons.lock_outlined),
|
||||
hintText: '****',
|
||||
errorText: passwordError,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: L10n.of(context).showPassword,
|
||||
icon: Icon(showPassword
|
||||
? Icons.visibility_off_outlined
|
||||
: Icons.visibility_outlined),
|
||||
onPressed: () =>
|
||||
setState(() => showPassword = !showPassword),
|
||||
),
|
||||
labelText: L10n.of(context).password),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Hero(
|
||||
tag: 'loginButton',
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: ElevatedButton(
|
||||
onPressed: loading ? null : () => login(context),
|
||||
child: loading
|
||||
? LinearProgressIndicator()
|
||||
: Text(
|
||||
L10n.of(context).login.toUpperCase(),
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => _passwordForgotten(context),
|
||||
child: Text(
|
||||
L10n.of(context).passwordForgotten,
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on String {
|
||||
static final RegExp _phoneRegex =
|
||||
RegExp(r'^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$');
|
||||
bool get isEmail => EmailValidator.validate(this);
|
||||
bool get isPhoneNumber => _phoneRegex.hasMatch(this);
|
||||
}
|
||||
54
lib/pages/views/new_group_ui.dart
Normal file
54
lib/pages/views/new_group_ui.dart
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:fluffychat/pages/new_group.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class NewGroupUI extends StatelessWidget {
|
||||
final NewGroupController controller;
|
||||
|
||||
const NewGroupUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).createNewGroup),
|
||||
elevation: 0,
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
controller: controller.controller,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
textInputAction: TextInputAction.go,
|
||||
onSubmitted: controller.submitAction,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).optionalGroupName,
|
||||
prefixIcon: Icon(Icons.people_outlined),
|
||||
hintText: L10n.of(context).enterAGroupName),
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(L10n.of(context).groupIsPublic),
|
||||
value: controller.publicGroup,
|
||||
onChanged: controller.setPublicGroup,
|
||||
),
|
||||
Expanded(
|
||||
child: Image.asset('assets/new_group_wallpaper.png'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: controller.submitAction,
|
||||
child: Icon(Icons.arrow_forward_outlined),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
133
lib/pages/views/new_private_chat_ui.dart
Normal file
133
lib/pages/views/new_private_chat_ui.dart
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:fluffychat/pages/new_private_chat.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/contacts_list.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
||||
class NewPrivateChatUI extends StatelessWidget {
|
||||
final NewPrivateChatController controller;
|
||||
|
||||
const NewPrivateChatUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).newChat),
|
||||
elevation: 0,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => AdaptivePageLayout.of(context)
|
||||
.pushNamedAndRemoveUntilIsFirst('/newgroup'),
|
||||
child: Text(
|
||||
L10n.of(context).createNewGroup,
|
||||
style: TextStyle(color: Theme.of(context).accentColor),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: TextFormField(
|
||||
controller: controller.controller,
|
||||
autocorrect: false,
|
||||
onChanged: controller.searchUserWithCoolDown,
|
||||
textInputAction: TextInputAction.go,
|
||||
onFieldSubmitted: controller.submitAction,
|
||||
validator: controller.validateForm,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).enterAUsername,
|
||||
prefixIcon: controller.loading
|
||||
? Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: controller.correctMxId
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Avatar(
|
||||
controller.foundProfile.avatarUrl,
|
||||
controller.foundProfile.displayname ??
|
||||
controller.foundProfile.userId,
|
||||
size: 12,
|
||||
),
|
||||
)
|
||||
: Icon(Icons.account_circle_outlined),
|
||||
prefixText: '@',
|
||||
suffixIcon: IconButton(
|
||||
onPressed: controller.submitAction,
|
||||
icon: Icon(Icons.arrow_forward_outlined),
|
||||
),
|
||||
hintText: '${L10n.of(context).username.toLowerCase()}',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(height: 1),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
radius: Avatar.defaultSize / 2,
|
||||
foregroundColor: Theme.of(context).accentColor,
|
||||
backgroundColor: Theme.of(context).secondaryHeaderColor,
|
||||
child: Icon(Icons.share_outlined),
|
||||
),
|
||||
onTap: controller.inviteAction,
|
||||
title: Text('${L10n.of(context).yourOwnUsername}:'),
|
||||
subtitle: Text(
|
||||
Matrix.of(context).client.userID,
|
||||
style: TextStyle(color: Theme.of(context).accentColor),
|
||||
),
|
||||
),
|
||||
Divider(height: 1),
|
||||
if (controller.foundProfiles.isNotEmpty)
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.foundProfiles.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final foundProfile = controller.foundProfiles[i];
|
||||
return ListTile(
|
||||
onTap: () => controller.pickUser(foundProfile),
|
||||
leading: Avatar(
|
||||
foundProfile.avatarUrl,
|
||||
foundProfile.displayname ?? foundProfile.userId,
|
||||
//size: 24,
|
||||
),
|
||||
title: Text(
|
||||
foundProfile.displayname ??
|
||||
foundProfile.userId.localpart,
|
||||
style: TextStyle(),
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
foundProfile.userId,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (controller.foundProfiles.isEmpty)
|
||||
Expanded(
|
||||
child: ContactsList(searchController: controller.controller),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
272
lib/pages/views/search_ui.dart
Normal file
272
lib/pages/views/search_ui.dart
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/contacts_list.dart';
|
||||
import 'package:fluffychat/widgets/default_app_bar_search_field.dart';
|
||||
import 'package:fluffychat/widgets/list_items/chat_list_item.dart';
|
||||
import 'package:fluffychat/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 '../search.dart';
|
||||
|
||||
class SearchUI extends StatelessWidget {
|
||||
final SearchController controller;
|
||||
|
||||
const SearchUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final server = controller.genericSearchTerm?.isValidMatrixId ?? false
|
||||
? controller.genericSearchTerm.domain
|
||||
: controller.server;
|
||||
if (controller.lastServer != server) {
|
||||
controller.lastServer = server;
|
||||
controller.publicRoomsResponse = null;
|
||||
}
|
||||
controller.publicRoomsResponse ??= Matrix.of(context)
|
||||
.client
|
||||
.queryPublicRooms(
|
||||
server: server,
|
||||
genericSearchTerm: controller.genericSearchTerm,
|
||||
)
|
||||
.catchError((error) {
|
||||
if (controller.widget.alias == null) {
|
||||
throw error;
|
||||
}
|
||||
return PublicRoomsResponse.fromJson({
|
||||
'chunk': [],
|
||||
});
|
||||
}).then((PublicRoomsResponse res) {
|
||||
if (controller.widget.alias != null &&
|
||||
!res.chunk.any((room) =>
|
||||
(room.aliases?.contains(controller.widget.alias) ?? false) ||
|
||||
room.canonicalAlias == controller.widget.alias)) {
|
||||
// we have to tack on the original alias
|
||||
res.chunk.add(PublicRoom.fromJson(<String, dynamic>{
|
||||
'aliases': [controller.widget.alias],
|
||||
'name': controller.widget.alias,
|
||||
}));
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
final rooms = List<Room>.from(Matrix.of(context).client.rooms);
|
||||
rooms.removeWhere(
|
||||
(room) =>
|
||||
room.lastEvent == null ||
|
||||
!room.displayname
|
||||
.toLowerCase()
|
||||
.contains(controller.controller.text.toLowerCase()),
|
||||
);
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
initialIndex: 1,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
titleSpacing: 0,
|
||||
title: DefaultAppBarSearchField(
|
||||
autofocus: true,
|
||||
hintText: L10n.of(context).search,
|
||||
searchController: controller.controller,
|
||||
suffix: Icon(Icons.search_outlined),
|
||||
onChanged: controller.search,
|
||||
),
|
||||
bottom: TabBar(
|
||||
indicatorColor: Theme.of(context).accentColor,
|
||||
labelColor: Theme.of(context).accentColor,
|
||||
unselectedLabelColor: Theme.of(context).textTheme.bodyText1.color,
|
||||
labelStyle: TextStyle(fontSize: 16),
|
||||
labelPadding: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 0,
|
||||
),
|
||||
tabs: [
|
||||
Tab(child: Text(L10n.of(context).discover, maxLines: 1)),
|
||||
Tab(child: Text(L10n.of(context).chats, maxLines: 1)),
|
||||
Tab(child: Text(L10n.of(context).people, maxLines: 1)),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
ListView(
|
||||
children: [
|
||||
SizedBox(height: 12),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: Theme.of(context).accentColor,
|
||||
backgroundColor: Theme.of(context).secondaryHeaderColor,
|
||||
child: Icon(Icons.edit_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).changeTheServer),
|
||||
onTap: controller.setServer,
|
||||
),
|
||||
FutureBuilder<PublicRoomsResponse>(
|
||||
future: controller.publicRoomsResponse,
|
||||
builder: (BuildContext context,
|
||||
AsyncSnapshot<PublicRoomsResponse> snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 32),
|
||||
Icon(
|
||||
Icons.error_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
snapshot.error.toLocalizedString(context),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final publicRoomsResponse = snapshot.data;
|
||||
if (publicRoomsResponse.chunk.isEmpty) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 32),
|
||||
Icon(
|
||||
Icons.search_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
L10n.of(context).noPublicRoomsFound,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.all(12),
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 1,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: publicRoomsResponse.chunk.length,
|
||||
itemBuilder: (BuildContext context, int i) => Material(
|
||||
elevation: 2,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
onTap: () => controller.joinGroupAction(
|
||||
publicRoomsResponse.chunk[i],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Avatar(
|
||||
Uri.parse(publicRoomsResponse
|
||||
.chunk[i].avatarUrl ??
|
||||
''),
|
||||
publicRoomsResponse.chunk[i].name),
|
||||
Text(
|
||||
publicRoomsResponse.chunk[i].name,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).countParticipants(
|
||||
publicRoomsResponse
|
||||
.chunk[i].numJoinedMembers ??
|
||||
0),
|
||||
style: TextStyle(fontSize: 10.5),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
publicRoomsResponse.chunk[i].topic ??
|
||||
L10n.of(context).noDescription,
|
||||
maxLines: 4,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
ListView.builder(
|
||||
itemCount: rooms.length,
|
||||
itemBuilder: (_, i) => ChatListItem(rooms[i]),
|
||||
),
|
||||
controller.foundProfiles.isNotEmpty
|
||||
? ListView.builder(
|
||||
itemCount: controller.foundProfiles.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final foundProfile = controller.foundProfiles[i];
|
||||
return ListTile(
|
||||
onTap: () async {
|
||||
final roomID = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context)
|
||||
.client
|
||||
.startDirectChat(foundProfile.userId),
|
||||
);
|
||||
if (roomID.error == null) {
|
||||
await AdaptivePageLayout.of(context)
|
||||
.popAndPushNamed('/rooms/${roomID.result}');
|
||||
}
|
||||
},
|
||||
leading: Avatar(
|
||||
foundProfile.avatarUrl,
|
||||
foundProfile.displayname ?? foundProfile.userId,
|
||||
//size: 24,
|
||||
),
|
||||
title: Text(
|
||||
foundProfile.displayname ??
|
||||
foundProfile.userId.localpart,
|
||||
style: TextStyle(),
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
foundProfile.userId,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: ContactsList(searchController: controller.controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
104
lib/pages/views/settings_3pid_ui.dart
Normal file
104
lib/pages/views/settings_3pid_ui.dart
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/settings_3pid.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class Settings3PidUI extends StatelessWidget {
|
||||
final Settings3PidController controller;
|
||||
|
||||
const Settings3PidUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
controller.request ??= Matrix.of(context).client.getAccount3PIDs();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).passwordRecovery),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.add_outlined),
|
||||
onPressed: controller.add3PidAction,
|
||||
tooltip: L10n.of(context).addEmail,
|
||||
)
|
||||
],
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: FutureBuilder<List<ThirdPartyIdentifier>>(
|
||||
future: controller.request,
|
||||
builder: (BuildContext context,
|
||||
AsyncSnapshot<List<ThirdPartyIdentifier>> snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
snapshot.error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final identifier = snapshot.data;
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor:
|
||||
identifier.isEmpty ? Colors.orange : Colors.grey,
|
||||
child: Icon(
|
||||
identifier.isEmpty
|
||||
? Icons.warning_outlined
|
||||
: Icons.info_outlined,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
identifier.isEmpty
|
||||
? L10n.of(context).noPasswordRecoveryDescription
|
||||
: L10n.of(context)
|
||||
.withTheseAddressesRecoveryDescription,
|
||||
),
|
||||
),
|
||||
Divider(height: 1),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: identifier.length,
|
||||
itemBuilder: (BuildContext context, int i) => ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(identifier[i].iconData)),
|
||||
title: Text(identifier[i].address),
|
||||
trailing: IconButton(
|
||||
tooltip: L10n.of(context).delete,
|
||||
icon: Icon(Icons.delete_forever_outlined),
|
||||
color: Colors.red,
|
||||
onPressed: () => controller.delete3Pid(identifier[i]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on ThirdPartyIdentifier {
|
||||
IconData get iconData {
|
||||
switch (medium) {
|
||||
case ThirdPartyIdentifierMedium.email:
|
||||
return Icons.mail_outline_rounded;
|
||||
case ThirdPartyIdentifierMedium.msisdn:
|
||||
return Icons.phone_android_outlined;
|
||||
}
|
||||
return Icons.device_unknown_outlined;
|
||||
}
|
||||
}
|
||||
230
lib/pages/views/settings_emotes_ui.dart
Normal file
230
lib/pages/views/settings_emotes_ui.dart
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../settings_emotes.dart';
|
||||
|
||||
class EmotesSettingsUI extends StatelessWidget {
|
||||
final EmotesSettingsController controller;
|
||||
|
||||
const EmotesSettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).emoteSettings),
|
||||
),
|
||||
floatingActionButton: controller.showSave
|
||||
? FloatingActionButton(
|
||||
onPressed: controller.saveAction,
|
||||
child: Icon(Icons.save_outlined, color: Colors.white),
|
||||
)
|
||||
: null,
|
||||
body: MaxWidthBody(
|
||||
child: StreamBuilder(
|
||||
stream: controller.widget.room?.onUpdate?.stream,
|
||||
builder: (context, snapshot) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (!controller.readonly)
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 180.0,
|
||||
height: 38,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller.newEmoteController,
|
||||
autocorrect: false,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context).emoteShortcode,
|
||||
prefixText: ': ',
|
||||
suffixText: ':',
|
||||
prefixStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
suffixStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: _EmoteImagePicker(
|
||||
controller: controller.newMxcController,
|
||||
onPressed: controller.emoteImagePickerAction,
|
||||
),
|
||||
trailing: InkWell(
|
||||
onTap: controller.addEmoteAction,
|
||||
child: Icon(
|
||||
Icons.add_outlined,
|
||||
color: Colors.green,
|
||||
size: 32.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.widget.room != null)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).enableEmotesGlobally),
|
||||
trailing: Switch(
|
||||
value: controller.isGloballyActive(client),
|
||||
onChanged: controller.setIsGloballyActive,
|
||||
),
|
||||
),
|
||||
if (!controller.readonly || controller.widget.room != null)
|
||||
Divider(
|
||||
height: 2,
|
||||
thickness: 2,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
Expanded(
|
||||
child: controller.emotes.isEmpty
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text(
|
||||
L10n.of(context).noEmotesFound,
|
||||
style: TextStyle(fontSize: 20),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
separatorBuilder: (BuildContext context, int i) =>
|
||||
Container(),
|
||||
itemCount: controller.emotes.length + 1,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
if (i >= controller.emotes.length) {
|
||||
return Container(height: 70);
|
||||
}
|
||||
final emote = controller.emotes[i];
|
||||
final textEditingController =
|
||||
TextEditingController();
|
||||
textEditingController.text = emote.emoteClean;
|
||||
return ListTile(
|
||||
leading: Container(
|
||||
width: 180.0,
|
||||
height: 38,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(10)),
|
||||
color:
|
||||
Theme.of(context).secondaryHeaderColor,
|
||||
),
|
||||
child: TextField(
|
||||
readOnly: controller.readonly,
|
||||
controller: textEditingController,
|
||||
autocorrect: false,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context).emoteShortcode,
|
||||
prefixText: ': ',
|
||||
suffixText: ':',
|
||||
prefixStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
suffixStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onSubmitted: (s) =>
|
||||
controller.submitEmoteAction(
|
||||
s,
|
||||
emote,
|
||||
textEditingController,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: _EmoteImage(emote.mxc),
|
||||
trailing: controller.readonly
|
||||
? null
|
||||
: InkWell(
|
||||
onTap: () =>
|
||||
controller.removeEmoteAction(emote),
|
||||
child: Icon(
|
||||
Icons.delete_forever_outlined,
|
||||
color: Colors.red,
|
||||
size: 32.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmoteImage extends StatelessWidget {
|
||||
final String mxc;
|
||||
_EmoteImage(this.mxc);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = 38.0;
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
final url = Uri.parse(mxc)?.getThumbnail(
|
||||
Matrix.of(context).client,
|
||||
width: size * devicePixelRatio,
|
||||
height: size * devicePixelRatio,
|
||||
method: ThumbnailMethod.scale,
|
||||
);
|
||||
return CachedNetworkImage(
|
||||
imageUrl: url.toString(),
|
||||
fit: BoxFit.contain,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmoteImagePicker extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
|
||||
final void Function(TextEditingController) onPressed;
|
||||
|
||||
_EmoteImagePicker({@required this.controller, @required this.onPressed});
|
||||
|
||||
@override
|
||||
_EmoteImagePickerState createState() => _EmoteImagePickerState();
|
||||
}
|
||||
|
||||
class _EmoteImagePickerState extends State<_EmoteImagePicker> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.controller.text == null || widget.controller.text.isEmpty) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async {},
|
||||
child: Text(L10n.of(context).pickImage),
|
||||
);
|
||||
} else {
|
||||
return _EmoteImage(widget.controller.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
lib/pages/views/settings_ignore_list_ui.dart
Normal file
94
lib/pages/views/settings_ignore_list_ui.dart
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../settings_ignore_list.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
|
||||
class SettingsIgnoreListUI extends StatelessWidget {
|
||||
final SettingsIgnoreListController controller;
|
||||
|
||||
const SettingsIgnoreListUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).ignoredUsers),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller.controller,
|
||||
autocorrect: false,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => controller.ignoreUser(context),
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'bad_guy:domain.abc',
|
||||
prefixText: '@',
|
||||
labelText: L10n.of(context).ignoreUsername,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: L10n.of(context).ignore,
|
||||
icon: Icon(Icons.done_outlined),
|
||||
onPressed: () => controller.ignoreUser(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
L10n.of(context).ignoreListDescription,
|
||||
style: TextStyle(color: Colors.orange),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(height: 1),
|
||||
Expanded(
|
||||
child: StreamBuilder<Object>(
|
||||
stream: client.onAccountData.stream
|
||||
.where((a) => a.type == 'm.ignored_user_list'),
|
||||
builder: (context, snapshot) {
|
||||
return ListView.builder(
|
||||
itemCount: client.ignoredUsers.length,
|
||||
itemBuilder: (c, i) => FutureBuilder<Profile>(
|
||||
future:
|
||||
client.getProfileFromUserId(client.ignoredUsers[i]),
|
||||
builder: (c, s) => ListTile(
|
||||
leading: Avatar(
|
||||
s.data?.avatarUrl ?? Uri.parse(''),
|
||||
s.data?.displayname ?? client.ignoredUsers[i],
|
||||
),
|
||||
title: Text(
|
||||
s.data?.displayname ?? client.ignoredUsers[i]),
|
||||
trailing: IconButton(
|
||||
tooltip: L10n.of(context).delete,
|
||||
icon: Icon(Icons.delete_forever_outlined),
|
||||
onPressed: () => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
client.unignoreUser(client.ignoredUsers[i]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
62
lib/pages/views/settings_multiple_emotes_ui.dart
Normal file
62
lib/pages/views/settings_multiple_emotes_ui.dart
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:fluffychat/pages/settings_multiple_emotes.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class MultipleEmotesSettingsUI extends StatelessWidget {
|
||||
final MultipleEmotesSettingsController controller;
|
||||
|
||||
MultipleEmotesSettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room =
|
||||
Matrix.of(context).client.getRoomById(controller.widget.roomId);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).emotePacks),
|
||||
),
|
||||
body: StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
final packs =
|
||||
room.states['im.ponies.room_emotes'] ?? <String, Event>{};
|
||||
if (!packs.containsKey('')) {
|
||||
packs[''] = null;
|
||||
}
|
||||
final keys = packs.keys.toList();
|
||||
keys.sort();
|
||||
return ListView.separated(
|
||||
separatorBuilder: (BuildContext context, int i) => Container(),
|
||||
itemCount: keys.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final event = packs[keys[i]];
|
||||
var packName = keys[i].isNotEmpty ? keys[i] : 'Default Pack';
|
||||
if (event != null && event.content['pack'] is Map) {
|
||||
if (event.content['pack']['displayname'] is String) {
|
||||
packName = event.content['pack']['displayname'];
|
||||
} else if (event.content['pack']['name'] is String) {
|
||||
packName = event.content['pack']['name'];
|
||||
}
|
||||
}
|
||||
return ListTile(
|
||||
title: Text(packName),
|
||||
onTap: () async {
|
||||
await AdaptivePageLayout.of(context).pushNamed(
|
||||
'/settings/emotes',
|
||||
arguments: {
|
||||
'room': room,
|
||||
'stateKey': keys[i],
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
123
lib/pages/views/settings_notifications_ui.dart
Normal file
123
lib/pages/views/settings_notifications_ui.dart
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
|
||||
import '../settings_notifications.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
|
||||
class SettingsNotificationsUI extends StatelessWidget {
|
||||
final SettingsNotificationsController controller;
|
||||
|
||||
const SettingsNotificationsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).notifications),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: StreamBuilder(
|
||||
stream: Matrix.of(context)
|
||||
.client
|
||||
.onAccountData
|
||||
.stream
|
||||
.where((event) => event.type == 'm.push_rules'),
|
||||
builder: (BuildContext context, _) {
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
value: !Matrix.of(context).client.allPushNotificationsMuted,
|
||||
title: Text(
|
||||
L10n.of(context).notificationsEnabledForThisAccount),
|
||||
onChanged: (_) => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
Matrix.of(context).client.setMuteAllPushNotifications(
|
||||
!Matrix.of(context)
|
||||
.client
|
||||
.allPushNotificationsMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!Matrix.of(context).client.allPushNotificationsMuted) ...{
|
||||
if (!kIsWeb && Platform.isAndroid)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).soundVibrationLedColor),
|
||||
trailing: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(Icons.edit_outlined),
|
||||
),
|
||||
onTap: controller.openAndroidNotificationSettingsAction,
|
||||
),
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).pushRules,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (var item in NotificationSettingsItem.items)
|
||||
SwitchListTile(
|
||||
value: controller.getNotificationSetting(item) ?? true,
|
||||
title: Text(item.title(context)),
|
||||
onChanged: (bool enabled) =>
|
||||
controller.setNotificationSetting(item, enabled),
|
||||
),
|
||||
},
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).devices,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
FutureBuilder<List<Pusher>>(
|
||||
future: Matrix.of(context).client.getPushers(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
Center(
|
||||
child: Text(
|
||||
snapshot.error.toLocalizedString(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final pushers = snapshot.data ?? [];
|
||||
return ListView.builder(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: pushers.length,
|
||||
itemBuilder: (_, i) => ListTile(
|
||||
title: Text(
|
||||
'${pushers[i].appDisplayName} - ${pushers[i].appId}'),
|
||||
subtitle: Text(pushers[i].data.url.toString()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
118
lib/pages/views/settings_style_ui.dart
Normal file
118
lib/pages/views/settings_style_ui.dart
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../../config/app_config.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../settings_style.dart';
|
||||
|
||||
class SettingsStyleUI extends StatelessWidget {
|
||||
final SettingsStyleController controller;
|
||||
|
||||
const SettingsStyleUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
controller.currentTheme ??= AdaptiveTheme.of(context).mode;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).changeTheme),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: Column(
|
||||
children: [
|
||||
RadioListTile<AdaptiveThemeMode>(
|
||||
groupValue: controller.currentTheme,
|
||||
value: AdaptiveThemeMode.system,
|
||||
title: Text(L10n.of(context).systemTheme),
|
||||
onChanged: controller.switchTheme,
|
||||
),
|
||||
RadioListTile<AdaptiveThemeMode>(
|
||||
groupValue: controller.currentTheme,
|
||||
value: AdaptiveThemeMode.light,
|
||||
title: Text(L10n.of(context).lightTheme),
|
||||
onChanged: controller.switchTheme,
|
||||
),
|
||||
RadioListTile<AdaptiveThemeMode>(
|
||||
groupValue: controller.currentTheme,
|
||||
value: AdaptiveThemeMode.dark,
|
||||
title: Text(L10n.of(context).darkTheme),
|
||||
onChanged: controller.switchTheme,
|
||||
),
|
||||
Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).wallpaper,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (Matrix.of(context).wallpaper != null)
|
||||
ListTile(
|
||||
title: Image.file(
|
||||
Matrix.of(context).wallpaper,
|
||||
height: 38,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.delete_forever_outlined,
|
||||
color: Colors.red,
|
||||
),
|
||||
onTap: controller.deleteWallpaperAction,
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
return ListTile(
|
||||
title: Text(L10n.of(context).changeWallpaper),
|
||||
trailing: Icon(Icons.wallpaper_outlined),
|
||||
onTap: controller.setWallpaperAction,
|
||||
);
|
||||
}),
|
||||
Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).fontSize,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Text('(*${AppConfig.fontSizeFactor})'),
|
||||
),
|
||||
Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 6, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.bodyText1.fontSize *
|
||||
AppConfig.fontSizeFactor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
min: 0.5,
|
||||
max: 2.5,
|
||||
divisions: 4,
|
||||
value: AppConfig.fontSizeFactor,
|
||||
semanticFormatterCallback: (d) => d.toString(),
|
||||
onChanged: controller.changeFontSizeFactor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
261
lib/pages/views/settings_ui.dart
Normal file
261
lib/pages/views/settings_ui.dart
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:fluffychat/widgets/sentry_switch_list_tile.dart';
|
||||
import 'package:fluffychat/widgets/settings_switch_list_tile.dart';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/utils/beautify_string_extension.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../widgets/content_banner.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../../config/app_config.dart';
|
||||
import '../../config/setting_keys.dart';
|
||||
import '../settings.dart';
|
||||
|
||||
class SettingsUI extends StatelessWidget {
|
||||
final SettingsController controller;
|
||||
|
||||
const SettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
return Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) =>
|
||||
<Widget>[
|
||||
SliverAppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.close_outlined),
|
||||
onPressed: () => AdaptivePageLayout.of(context).popUntilIsFirst(),
|
||||
),
|
||||
expandedHeight: 300.0,
|
||||
floating: true,
|
||||
pinned: true,
|
||||
title: Text(L10n.of(context).settings),
|
||||
actions: [
|
||||
FutureBuilder(
|
||||
future: controller.crossSigningCachedFuture,
|
||||
builder: (context, snapshot) {
|
||||
final needsBootstrap = Matrix.of(context)
|
||||
.client
|
||||
.encryption
|
||||
?.crossSigning
|
||||
?.enabled ==
|
||||
false ||
|
||||
snapshot.data == false;
|
||||
final isUnknownSession =
|
||||
Matrix.of(context).client.isUnknownSession;
|
||||
final displayHeader = needsBootstrap || isUnknownSession;
|
||||
if (!displayHeader) return Container();
|
||||
return TextButton.icon(
|
||||
icon: Icon(Icons.cloud, color: Colors.red),
|
||||
label: Text(
|
||||
L10n.of(context).chatBackup,
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
onPressed: controller.firstRunBootstrapAction,
|
||||
);
|
||||
}),
|
||||
],
|
||||
backgroundColor: Theme.of(context).appBarTheme.color,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: ContentBanner(controller.profile?.avatarUrl,
|
||||
onEdit: controller.setAvatarAction),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).notifications,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.notifications_outlined),
|
||||
title: Text(L10n.of(context).notifications),
|
||||
onTap: () => AdaptivePageLayout.of(context)
|
||||
.pushNamed('/settings/notifications'),
|
||||
),
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).chat,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).changeTheme),
|
||||
onTap: () =>
|
||||
AdaptivePageLayout.of(context).pushNamed('/settings/style'),
|
||||
trailing: Icon(Icons.style_outlined),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
title: L10n.of(context).renderRichContent,
|
||||
onChanged: (b) => AppConfig.renderHtml = b,
|
||||
storeKey: SettingKeys.renderHtml,
|
||||
defaultValue: AppConfig.renderHtml,
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
title: L10n.of(context).hideRedactedEvents,
|
||||
onChanged: (b) => AppConfig.hideRedactedEvents = b,
|
||||
storeKey: SettingKeys.hideRedactedEvents,
|
||||
defaultValue: AppConfig.hideRedactedEvents,
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
title: L10n.of(context).hideUnknownEvents,
|
||||
onChanged: (b) => AppConfig.hideUnknownEvents = b,
|
||||
storeKey: SettingKeys.hideUnknownEvents,
|
||||
defaultValue: AppConfig.hideUnknownEvents,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).emoteSettings),
|
||||
onTap: () =>
|
||||
AdaptivePageLayout.of(context).pushNamed('/settings/emotes'),
|
||||
trailing: Icon(Icons.insert_emoticon_outlined),
|
||||
),
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).account,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.edit_outlined),
|
||||
title: Text(L10n.of(context).editDisplayname),
|
||||
subtitle: Text(
|
||||
controller.profile?.displayname ?? client.userID.localpart),
|
||||
onTap: controller.setDisplaynameAction,
|
||||
),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.phone_outlined),
|
||||
title: Text(L10n.of(context).editJitsiInstance),
|
||||
subtitle: Text(AppConfig.jitsiInstance),
|
||||
onTap: controller.setJitsiInstanceAction,
|
||||
),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.devices_other_outlined),
|
||||
title: Text(L10n.of(context).devices),
|
||||
onTap: () =>
|
||||
AdaptivePageLayout.of(context).pushNamed('/settings/devices'),
|
||||
),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.block_outlined),
|
||||
title: Text(L10n.of(context).ignoredUsers),
|
||||
onTap: () =>
|
||||
AdaptivePageLayout.of(context).pushNamed('/settings/ignore'),
|
||||
),
|
||||
SentrySwitchListTile(),
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.security_outlined),
|
||||
title: Text(
|
||||
L10n.of(context).changePassword,
|
||||
),
|
||||
onTap: controller.changePasswordAccountAction,
|
||||
),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.email_outlined),
|
||||
title: Text(L10n.of(context).passwordRecovery),
|
||||
onTap: () =>
|
||||
AdaptivePageLayout.of(context).pushNamed('/settings/3pid'),
|
||||
),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.exit_to_app_outlined),
|
||||
title: Text(L10n.of(context).logout),
|
||||
onTap: controller.logoutAction,
|
||||
),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.delete_forever_outlined),
|
||||
title: Text(
|
||||
L10n.of(context).deleteAccount,
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
onTap: controller.deleteAccountAction,
|
||||
),
|
||||
if (client.encryption != null) ...{
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).security,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (PlatformInfos.isMobile)
|
||||
ListTile(
|
||||
trailing: Icon(Icons.lock_outlined),
|
||||
title: Text(L10n.of(context).appLock),
|
||||
onTap: controller.setAppLockAction,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).yourPublicKey),
|
||||
onTap: () => showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).yourPublicKey,
|
||||
message: client.fingerprintKey.beautified,
|
||||
okLabel: L10n.of(context).ok,
|
||||
useRootNavigator: false,
|
||||
),
|
||||
trailing: Icon(Icons.vpn_key_outlined),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).cachedKeys),
|
||||
trailing: Icon(Icons.wb_cloudy_outlined),
|
||||
subtitle: Text(
|
||||
'${client.encryption.keyManager.enabled ? L10n.of(context).onlineKeyBackupEnabled : L10n.of(context).onlineKeyBackupDisabled}\n${client.encryption.crossSigning.enabled ? L10n.of(context).crossSigningEnabled : L10n.of(context).crossSigningDisabled}'),
|
||||
onTap: controller.bootstrapSettingsAction,
|
||||
),
|
||||
},
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).about,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
onTap: () => AdaptivePageLayout.of(context).pushNamed('/logs'),
|
||||
),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.help_outlined),
|
||||
title: Text(L10n.of(context).help),
|
||||
onTap: () => launch(AppConfig.supportUrl),
|
||||
),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.privacy_tip_outlined),
|
||||
title: Text(L10n.of(context).privacy),
|
||||
onTap: () => launch(AppConfig.privacyUrl),
|
||||
),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.link_outlined),
|
||||
title: Text(L10n.of(context).about),
|
||||
onTap: () => PlatformInfos.showDialog(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
87
lib/pages/views/sign_up_password_ui.dart
Normal file
87
lib/pages/views/sign_up_password_ui.dart
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:fluffychat/pages/sign_up_password.dart';
|
||||
|
||||
import 'package:fluffychat/widgets/layouts/one_page_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class SignUpPasswordUI extends StatelessWidget {
|
||||
final SignUpPasswordController controller;
|
||||
|
||||
const SignUpPasswordUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OnePageCard(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
leading: controller.loading ? Container() : BackButton(),
|
||||
title: Text(
|
||||
L10n.of(context).chooseAStrongPassword,
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
controller: controller.passwordController,
|
||||
obscureText: !controller.showPassword,
|
||||
autofocus: true,
|
||||
readOnly: controller.loading,
|
||||
autocorrect: false,
|
||||
onSubmitted: (_) => controller.signUpAction,
|
||||
autofillHints:
|
||||
controller.loading ? null : [AutofillHints.newPassword],
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(Icons.lock_outlined),
|
||||
hintText: '****',
|
||||
errorText: controller.passwordError,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: L10n.of(context).showPassword,
|
||||
icon: Icon(controller.showPassword
|
||||
? Icons.visibility_off_outlined
|
||||
: Icons.visibility_outlined),
|
||||
onPressed: controller.toggleShowPassword,
|
||||
),
|
||||
labelText: L10n.of(context).password),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
controller: controller.emailController,
|
||||
readOnly: controller.loading,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onSubmitted: (_) => controller.signUpAction,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(Icons.mail_outline_outlined),
|
||||
errorText: controller.emailError,
|
||||
hintText: 'email@example.com',
|
||||
labelText: L10n.of(context).optionalAddEmail),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Hero(
|
||||
tag: 'loginButton',
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
controller.loading ? null : controller.signUpAction,
|
||||
child: controller.loading
|
||||
? LinearProgressIndicator()
|
||||
: Text(
|
||||
L10n.of(context).createAccountNow.toUpperCase(),
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
117
lib/pages/views/sign_up_ui.dart
Normal file
117
lib/pages/views/sign_up_ui.dart
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:fluffychat/pages/sign_up.dart';
|
||||
import 'package:fluffychat/widgets/fluffy_banner.dart';
|
||||
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/layouts/one_page_card.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class SignUpUI extends StatelessWidget {
|
||||
final SignUpController controller;
|
||||
|
||||
const SignUpUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OnePageCard(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
leading: controller.loading ? Container() : BackButton(),
|
||||
title: Text(
|
||||
Matrix.of(context)
|
||||
.client
|
||||
.homeserver
|
||||
.toString()
|
||||
.replaceFirst('https://', ''),
|
||||
),
|
||||
),
|
||||
body: ListView(children: <Widget>[
|
||||
Hero(
|
||||
tag: 'loginBanner',
|
||||
child: FluffyBanner(),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: TextField(
|
||||
readOnly: controller.loading,
|
||||
autocorrect: false,
|
||||
controller: controller.usernameController,
|
||||
onSubmitted: controller.signUpAction,
|
||||
autofillHints:
|
||||
controller.loading ? null : [AutofillHints.newUsername],
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(Icons.account_circle_outlined),
|
||||
hintText: L10n.of(context).username,
|
||||
errorText: controller.usernameError,
|
||||
labelText: L10n.of(context).chooseAUsername,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundImage: controller.avatar == null
|
||||
? null
|
||||
: MemoryImage(controller.avatar.bytes),
|
||||
backgroundColor: controller.avatar == null
|
||||
? Theme.of(context).brightness == Brightness.dark
|
||||
? Color(0xff121212)
|
||||
: Colors.white
|
||||
: Theme.of(context).secondaryHeaderColor,
|
||||
child: controller.avatar == null
|
||||
? Icon(Icons.camera_alt_outlined,
|
||||
color: Theme.of(context).primaryColor)
|
||||
: null,
|
||||
),
|
||||
trailing: controller.avatar == null
|
||||
? null
|
||||
: Icon(
|
||||
Icons.close,
|
||||
color: Colors.red,
|
||||
),
|
||||
title: Text(controller.avatar == null
|
||||
? L10n.of(context).setAProfilePicture
|
||||
: L10n.of(context).discardPicture),
|
||||
onTap: controller.avatar == null
|
||||
? controller.setAvatarAction
|
||||
: controller.resetAvatarAction,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Hero(
|
||||
tag: 'loginButton',
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.loading ? null : controller.signUpAction,
|
||||
child: controller.loading
|
||||
? LinearProgressIndicator()
|
||||
: Text(
|
||||
L10n.of(context).signUp.toUpperCase(),
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () =>
|
||||
AdaptivePageLayout.of(context).pushNamed('/login'),
|
||||
child: Text(
|
||||
L10n.of(context).alreadyHaveAnAccount,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.blue,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
157
lib/pages/views/user_bottom_sheet_ui.dart
Normal file
157
lib/pages/views/user_bottom_sheet_ui.dart
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../widgets/content_banner.dart';
|
||||
import '../user_bottom_sheet.dart';
|
||||
import '../../utils/presence_extension.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class UserBottomSheetUI extends StatelessWidget {
|
||||
final UserBottomSheetController controller;
|
||||
|
||||
const UserBottomSheetUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = controller.widget.user;
|
||||
final client = user.room.client;
|
||||
final presence = client.presences[user.id];
|
||||
return Center(
|
||||
child: Container(
|
||||
width: min(
|
||||
MediaQuery.of(context).size.width, FluffyThemes.columnWidth * 1.5),
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5),
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_downward_outlined),
|
||||
onPressed: Navigator.of(context, rootNavigator: false).pop,
|
||||
tooltip: L10n.of(context).close,
|
||||
),
|
||||
title: Text(user.calcDisplayname()),
|
||||
actions: [
|
||||
if (user.id != user.room.client.userID)
|
||||
PopupMenuButton(
|
||||
itemBuilder: (_) => [
|
||||
if (controller.widget.onMention != null)
|
||||
PopupMenuItem(
|
||||
value: 'mention',
|
||||
child: _TextWithIcon(
|
||||
L10n.of(context).mention,
|
||||
Icons.alternate_email_outlined,
|
||||
),
|
||||
),
|
||||
if (user.id != user.room.client.userID &&
|
||||
!user.room.isDirectChat)
|
||||
PopupMenuItem(
|
||||
value: 'message',
|
||||
child: _TextWithIcon(
|
||||
L10n.of(context).sendAMessage,
|
||||
Icons.send_outlined,
|
||||
),
|
||||
),
|
||||
if (user.canChangePowerLevel)
|
||||
PopupMenuItem(
|
||||
value: 'permission',
|
||||
child: _TextWithIcon(
|
||||
L10n.of(context).setPermissionsLevel,
|
||||
Icons.edit_attributes_outlined,
|
||||
),
|
||||
),
|
||||
if (user.canKick)
|
||||
PopupMenuItem(
|
||||
value: 'kick',
|
||||
child: _TextWithIcon(
|
||||
L10n.of(context).kickFromChat,
|
||||
Icons.exit_to_app_outlined,
|
||||
),
|
||||
),
|
||||
if (user.canBan && user.membership != Membership.ban)
|
||||
PopupMenuItem(
|
||||
value: 'ban',
|
||||
child: _TextWithIcon(
|
||||
L10n.of(context).banFromChat,
|
||||
Icons.warning_sharp,
|
||||
),
|
||||
)
|
||||
else if (user.canBan &&
|
||||
user.membership == Membership.ban)
|
||||
PopupMenuItem(
|
||||
value: 'unban',
|
||||
child: _TextWithIcon(
|
||||
L10n.of(context).removeExile,
|
||||
Icons.warning_outlined,
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: controller.participantAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ContentBanner(
|
||||
user.avatarUrl,
|
||||
defaultIcon: Icons.person_outline,
|
||||
client: user.room.client,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).username),
|
||||
subtitle: Text(user.id),
|
||||
trailing: Icon(Icons.share_outlined),
|
||||
onTap: () => FluffyShare.share(
|
||||
user.id, controller.widget.outerContext),
|
||||
),
|
||||
if (presence != null)
|
||||
ListTile(
|
||||
title: Text(presence.getLocalizedStatusMessage(context)),
|
||||
subtitle:
|
||||
Text(presence.getLocalizedLastActiveAgo(context)),
|
||||
trailing: Icon(Icons.circle,
|
||||
color: presence.presence.currentlyActive ?? false
|
||||
? Colors.green
|
||||
: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TextWithIcon extends StatelessWidget {
|
||||
final String text;
|
||||
final IconData iconData;
|
||||
|
||||
const _TextWithIcon(
|
||||
this.text,
|
||||
this.iconData, {
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(iconData),
|
||||
SizedBox(width: 16),
|
||||
Text(text),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue