mirror of
https://github.com/krille-chan/fluffychat
synced 2024-09-10 05:45:10 +00:00
feat: new design
This commit is contained in:
parent
9a244a7302
commit
1d7d8dcae0
19 changed files with 3428 additions and 696 deletions
2169
10cf8daf25c0ff50974c0439cf89fa6528510012.diff
Normal file
2169
10cf8daf25c0ff50974c0439cf89fa6528510012.diff
Normal file
File diff suppressed because it is too large
Load diff
|
@ -9,6 +9,7 @@ class DefaultAppBarSearchField extends StatefulWidget {
|
||||||
final String hintText;
|
final String hintText;
|
||||||
final EdgeInsets padding;
|
final EdgeInsets padding;
|
||||||
final bool readOnly;
|
final bool readOnly;
|
||||||
|
final Widget prefixIcon;
|
||||||
|
|
||||||
const DefaultAppBarSearchField({
|
const DefaultAppBarSearchField({
|
||||||
Key key,
|
Key key,
|
||||||
|
@ -20,6 +21,7 @@ class DefaultAppBarSearchField extends StatefulWidget {
|
||||||
this.hintText,
|
this.hintText,
|
||||||
this.padding,
|
this.padding,
|
||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
|
this.prefixIcon,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -73,12 +75,18 @@ class _DefaultAppBarSearchFieldState extends State<DefaultAppBarSearchField> {
|
||||||
readOnly: widget.readOnly,
|
readOnly: widget.readOnly,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
prefixText: widget.prefixText,
|
prefixText: widget.prefixText,
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide:
|
||||||
|
BorderSide(color: Theme.of(context).secondaryHeaderColor),
|
||||||
|
),
|
||||||
contentPadding: EdgeInsets.only(
|
contentPadding: EdgeInsets.only(
|
||||||
top: 8,
|
top: 8,
|
||||||
bottom: 8,
|
bottom: 8,
|
||||||
left: 16,
|
left: 16,
|
||||||
),
|
),
|
||||||
hintText: widget.hintText,
|
hintText: widget.hintText,
|
||||||
|
prefixIcon: widget.prefixIcon,
|
||||||
suffixIcon: !widget.readOnly &&
|
suffixIcon: !widget.readOnly &&
|
||||||
(_focusNode.hasFocus ||
|
(_focusNode.hasFocus ||
|
||||||
(widget.suffix == null &&
|
(widget.suffix == null &&
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
|
||||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|
||||||
|
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
|
||||||
import 'matrix.dart';
|
|
||||||
|
|
||||||
class DefaultDrawer extends StatelessWidget {
|
|
||||||
void _drawerTapAction(BuildContext context, String route) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
AdaptivePageLayout.of(context).pushNamedAndRemoveUntilIsFirst(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setStatus(BuildContext context) async {
|
|
||||||
final client = Matrix.of(context).client;
|
|
||||||
final input = await showTextInputDialog(
|
|
||||||
title: L10n.of(context).setStatus,
|
|
||||||
context: context,
|
|
||||||
textFields: [
|
|
||||||
DialogTextField(
|
|
||||||
hintText: L10n.of(context).statusExampleMessage,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (input == null || input.single.isEmpty) return;
|
|
||||||
await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () => client.sendPresence(
|
|
||||||
client.userID,
|
|
||||||
PresenceType.online,
|
|
||||||
statusMsg: input.single,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Drawer(
|
|
||||||
child: SafeArea(
|
|
||||||
child: ListView(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
children: <Widget>[
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(Icons.edit_outlined),
|
|
||||||
title: Text(L10n.of(context).setStatus),
|
|
||||||
onTap: () => _setStatus(context),
|
|
||||||
),
|
|
||||||
Divider(height: 1),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(Icons.people_outline),
|
|
||||||
title: Text(L10n.of(context).createNewGroup),
|
|
||||||
onTap: () => _drawerTapAction(context, '/newgroup'),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(Icons.person_add_outlined),
|
|
||||||
title: Text(L10n.of(context).newPrivateChat),
|
|
||||||
onTap: () => _drawerTapAction(context, '/newprivatechat'),
|
|
||||||
),
|
|
||||||
Divider(height: 1),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(Icons.archive_outlined),
|
|
||||||
title: Text(L10n.of(context).archive),
|
|
||||||
onTap: () => _drawerTapAction(
|
|
||||||
context,
|
|
||||||
'/archive',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(Icons.group_work_outlined),
|
|
||||||
title: Text(L10n.of(context).discoverGroups),
|
|
||||||
onTap: () => _drawerTapAction(
|
|
||||||
context,
|
|
||||||
'/discover',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Divider(height: 1),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(Icons.settings_outlined),
|
|
||||||
title: Text(L10n.of(context).settings),
|
|
||||||
onTap: () => _drawerTapAction(
|
|
||||||
context,
|
|
||||||
'/settings',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
130
lib/components/list_items/status_list_tile.dart
Normal file
130
lib/components/list_items/status_list_tile.dart
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:fluffychat/components/avatar.dart';
|
||||||
|
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||||
|
import 'package:fluffychat/utils/status.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
|
||||||
|
import '../../utils/string_color.dart';
|
||||||
|
import '../../utils/date_time_extension.dart';
|
||||||
|
import '../matrix.dart';
|
||||||
|
|
||||||
|
class StatusListTile extends StatelessWidget {
|
||||||
|
final Status status;
|
||||||
|
|
||||||
|
const StatusListTile({Key key, @required this.status}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final text = status.message;
|
||||||
|
final isImage = text.startsWith('mxc://') && text.split(' ').length == 1;
|
||||||
|
return FutureBuilder<Profile>(
|
||||||
|
future: Matrix.of(context).client.getProfileFromUserId(status.senderId),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final displayname =
|
||||||
|
snapshot.data?.displayname ?? status.senderId.localpart;
|
||||||
|
final avatarUrl = snapshot.data?.avatarUrl;
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: Avatar(avatarUrl, displayname),
|
||||||
|
title: Text(
|
||||||
|
displayname,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
subtitle: Text(status.dateTime.localizedTime(context),
|
||||||
|
style: TextStyle(fontSize: 14)),
|
||||||
|
trailing: PopupMenuButton(
|
||||||
|
onSelected: (_) => AdaptivePageLayout.of(context).pushNamed(
|
||||||
|
'/settings/ignore',
|
||||||
|
arguments: status.senderId),
|
||||||
|
itemBuilder: (_) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text(L10n.of(context).ignore),
|
||||||
|
value: 'ignore',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isImage
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: Uri.parse(text).getThumbnail(
|
||||||
|
Matrix.of(context).client,
|
||||||
|
width: 360,
|
||||||
|
height: 360,
|
||||||
|
method: ThumbnailMethod.scale,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: double.infinity,
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
height: 256,
|
||||||
|
color: text.color,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 12.0, left: 12.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(CupertinoIcons.chat_bubble),
|
||||||
|
onPressed: () async {
|
||||||
|
final result = await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () => User(
|
||||||
|
status.senderId,
|
||||||
|
room:
|
||||||
|
Room(id: '', client: Matrix.of(context).client),
|
||||||
|
).startDirectChat(),
|
||||||
|
);
|
||||||
|
if (result.error == null) {
|
||||||
|
await AdaptivePageLayout.of(context)
|
||||||
|
.pushNamed('/rooms/${result.result}');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.ios_share),
|
||||||
|
onPressed: () => AdaptivePageLayout.of(context)
|
||||||
|
.pushNamed('/newstatus', arguments: status.message),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.share_outlined),
|
||||||
|
onPressed: () => FluffyShare.share(
|
||||||
|
'$displayname: ${status.message}',
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.delete_outlined),
|
||||||
|
onPressed: () => showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () => Matrix.of(context)
|
||||||
|
.removeStatusOfUser(status.senderId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import 'package:fluffychat/utils/firebase_controller.dart';
|
||||||
import 'package:fluffychat/utils/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_locals.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
import 'package:fluffychat/utils/sentry_controller.dart';
|
import 'package:fluffychat/utils/sentry_controller.dart';
|
||||||
|
import 'package:fluffychat/utils/status.dart';
|
||||||
import 'package:flushbar/flushbar.dart';
|
import 'package:flushbar/flushbar.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -126,6 +127,7 @@ class MatrixState extends State<Matrix> {
|
||||||
StreamSubscription onKeyVerificationRequestSub;
|
StreamSubscription onKeyVerificationRequestSub;
|
||||||
StreamSubscription onJitsiCallSub;
|
StreamSubscription onJitsiCallSub;
|
||||||
StreamSubscription onNotification;
|
StreamSubscription onNotification;
|
||||||
|
StreamSubscription<Presence> onPresence;
|
||||||
StreamSubscription<LoginState> onLoginStateChanged;
|
StreamSubscription<LoginState> onLoginStateChanged;
|
||||||
StreamSubscription<UiaRequest> onUiaRequest;
|
StreamSubscription<UiaRequest> onUiaRequest;
|
||||||
StreamSubscription<html.Event> onFocusSub;
|
StreamSubscription<html.Event> onFocusSub;
|
||||||
|
@ -288,6 +290,10 @@ class MatrixState extends State<Matrix> {
|
||||||
LoadingDialog.defaultBackLabel = L10n.of(context).close;
|
LoadingDialog.defaultBackLabel = L10n.of(context).close;
|
||||||
LoadingDialog.defaultOnError = (Object e) => e.toLocalizedString(context);
|
LoadingDialog.defaultOnError = (Object e) => e.toLocalizedString(context);
|
||||||
|
|
||||||
|
onPresence ??= client.onPresence.stream
|
||||||
|
.where((p) => p.presence?.statusMsg != null)
|
||||||
|
.listen(_onPresence);
|
||||||
|
|
||||||
onRoomKeyRequestSub ??=
|
onRoomKeyRequestSub ??=
|
||||||
client.onRoomKeyRequest.stream.listen((RoomKeyRequest request) async {
|
client.onRoomKeyRequest.stream.listen((RoomKeyRequest request) async {
|
||||||
final room = request.room;
|
final room = request.room;
|
||||||
|
@ -395,6 +401,45 @@ class MatrixState extends State<Matrix> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, Status> get statuses {
|
||||||
|
if (client.accountData.containsKey(Status.namespace)) {
|
||||||
|
try {
|
||||||
|
return client.accountData[Status.namespace].content
|
||||||
|
.map((k, v) => MapEntry(k, Status.fromJson(v)));
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs()
|
||||||
|
.e('Unable to parse status account data. Clearing up now...', e, s);
|
||||||
|
client.setAccountData(client.userID, Status.namespace, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPresence(Presence presence) async {
|
||||||
|
if (statuses[presence.senderId]?.message != presence.presence.statusMsg) {
|
||||||
|
Logs().v('Update status from ${presence.senderId}');
|
||||||
|
await client.setAccountData(
|
||||||
|
client.userID,
|
||||||
|
Status.namespace,
|
||||||
|
statuses.map((k, v) => MapEntry(k, v.toJson()))
|
||||||
|
..[presence.senderId] = Status(
|
||||||
|
presence.senderId,
|
||||||
|
presence.presence.statusMsg,
|
||||||
|
DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeStatusOfUser(String userId) async {
|
||||||
|
await client.setAccountData(
|
||||||
|
client.userID,
|
||||||
|
Status.namespace,
|
||||||
|
statuses.map((k, v) => MapEntry(k, v.toJson()))..remove(userId),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
onRoomKeyRequestSub?.cancel();
|
onRoomKeyRequestSub?.cancel();
|
||||||
|
@ -403,6 +448,7 @@ class MatrixState extends State<Matrix> {
|
||||||
onNotification?.cancel();
|
onNotification?.cancel();
|
||||||
onFocusSub?.cancel();
|
onFocusSub?.cancel();
|
||||||
onBlurSub?.cancel();
|
onBlurSub?.cancel();
|
||||||
|
onPresence?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,8 @@ import 'package:fluffychat/views/archive.dart';
|
||||||
import 'package:fluffychat/views/chat.dart';
|
import 'package:fluffychat/views/chat.dart';
|
||||||
import 'package:fluffychat/views/chat_details.dart';
|
import 'package:fluffychat/views/chat_details.dart';
|
||||||
import 'package:fluffychat/views/chat_encryption_settings.dart';
|
import 'package:fluffychat/views/chat_encryption_settings.dart';
|
||||||
import 'package:fluffychat/views/chat_list.dart';
|
import 'package:fluffychat/views/home_view.dart';
|
||||||
import 'package:fluffychat/views/chat_permissions_settings.dart';
|
import 'package:fluffychat/views/chat_permissions_settings.dart';
|
||||||
import 'package:fluffychat/views/discover_view.dart';
|
|
||||||
import 'package:fluffychat/views/empty_page.dart';
|
import 'package:fluffychat/views/empty_page.dart';
|
||||||
import 'package:fluffychat/views/homeserver_picker.dart';
|
import 'package:fluffychat/views/homeserver_picker.dart';
|
||||||
import 'package:fluffychat/views/invitation_selection.dart';
|
import 'package:fluffychat/views/invitation_selection.dart';
|
||||||
|
@ -16,6 +15,7 @@ import 'package:fluffychat/views/log_view.dart';
|
||||||
import 'package:fluffychat/views/login.dart';
|
import 'package:fluffychat/views/login.dart';
|
||||||
import 'package:fluffychat/views/new_group.dart';
|
import 'package:fluffychat/views/new_group.dart';
|
||||||
import 'package:fluffychat/views/new_private_chat.dart';
|
import 'package:fluffychat/views/new_private_chat.dart';
|
||||||
|
import 'package:fluffychat/views/set_status_view.dart';
|
||||||
import 'package:fluffychat/views/settings.dart';
|
import 'package:fluffychat/views/settings.dart';
|
||||||
import 'package:fluffychat/views/settings_3pid.dart';
|
import 'package:fluffychat/views/settings_3pid.dart';
|
||||||
import 'package:fluffychat/views/settings_devices.dart';
|
import 'package:fluffychat/views/settings_devices.dart';
|
||||||
|
@ -64,14 +64,14 @@ class FluffyRoutes {
|
||||||
switch (parts[1]) {
|
switch (parts[1]) {
|
||||||
case '':
|
case '':
|
||||||
return ViewData(
|
return ViewData(
|
||||||
mainView: (_) => ChatList(),
|
mainView: (_) => HomeView(),
|
||||||
emptyView: (_) => EmptyPage(),
|
emptyView: (_) => EmptyPage(),
|
||||||
);
|
);
|
||||||
case 'rooms':
|
case 'rooms':
|
||||||
final roomId = parts[2];
|
final roomId = parts[2];
|
||||||
if (parts.length == 3) {
|
if (parts.length == 3) {
|
||||||
return ViewData(
|
return ViewData(
|
||||||
leftView: (_) => ChatList(activeChat: roomId),
|
leftView: (_) => HomeView(activeChat: roomId),
|
||||||
mainView: (_) => Chat(roomId),
|
mainView: (_) => Chat(roomId),
|
||||||
);
|
);
|
||||||
} else if (parts.length == 4) {
|
} else if (parts.length == 4) {
|
||||||
|
@ -79,44 +79,44 @@ class FluffyRoutes {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'details':
|
case 'details':
|
||||||
return ViewData(
|
return ViewData(
|
||||||
leftView: (_) => ChatList(activeChat: roomId),
|
leftView: (_) => HomeView(activeChat: roomId),
|
||||||
mainView: (_) => Chat(roomId),
|
mainView: (_) => Chat(roomId),
|
||||||
rightView: (_) => ChatDetails(roomId),
|
rightView: (_) => ChatDetails(roomId),
|
||||||
);
|
);
|
||||||
case 'encryption':
|
case 'encryption':
|
||||||
return ViewData(
|
return ViewData(
|
||||||
leftView: (_) => ChatList(activeChat: roomId),
|
leftView: (_) => HomeView(activeChat: roomId),
|
||||||
mainView: (_) => Chat(roomId),
|
mainView: (_) => Chat(roomId),
|
||||||
rightView: (_) => ChatEncryptionSettings(roomId),
|
rightView: (_) => ChatEncryptionSettings(roomId),
|
||||||
);
|
);
|
||||||
case 'permissions':
|
case 'permissions':
|
||||||
return ViewData(
|
return ViewData(
|
||||||
leftView: (_) => ChatList(activeChat: roomId),
|
leftView: (_) => HomeView(activeChat: roomId),
|
||||||
mainView: (_) => Chat(roomId),
|
mainView: (_) => Chat(roomId),
|
||||||
rightView: (_) => ChatPermissionsSettings(roomId),
|
rightView: (_) => ChatPermissionsSettings(roomId),
|
||||||
);
|
);
|
||||||
case 'invite':
|
case 'invite':
|
||||||
return ViewData(
|
return ViewData(
|
||||||
leftView: (_) => ChatList(activeChat: roomId),
|
leftView: (_) => HomeView(activeChat: roomId),
|
||||||
mainView: (_) => Chat(roomId),
|
mainView: (_) => Chat(roomId),
|
||||||
rightView: (_) => InvitationSelection(roomId),
|
rightView: (_) => InvitationSelection(roomId),
|
||||||
);
|
);
|
||||||
case 'emotes':
|
case 'emotes':
|
||||||
return ViewData(
|
return ViewData(
|
||||||
leftView: (_) => ChatList(activeChat: roomId),
|
leftView: (_) => HomeView(activeChat: roomId),
|
||||||
mainView: (_) => Chat(roomId),
|
mainView: (_) => Chat(roomId),
|
||||||
rightView: (_) => MultipleEmotesSettings(roomId),
|
rightView: (_) => MultipleEmotesSettings(roomId),
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return ViewData(
|
return ViewData(
|
||||||
leftView: (_) => ChatList(activeChat: roomId),
|
leftView: (_) => HomeView(activeChat: roomId),
|
||||||
mainView: (_) => Chat(roomId,
|
mainView: (_) => Chat(roomId,
|
||||||
scrollToEventId: action.sigil == '\$' ? action : null),
|
scrollToEventId: action.sigil == '\$' ? action : null),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ViewData(
|
return ViewData(
|
||||||
mainView: (_) => ChatList(),
|
mainView: (_) => HomeView(),
|
||||||
emptyView: (_) => EmptyPage(),
|
emptyView: (_) => EmptyPage(),
|
||||||
);
|
);
|
||||||
case 'archive':
|
case 'archive':
|
||||||
|
@ -124,26 +124,25 @@ class FluffyRoutes {
|
||||||
mainView: (_) => Archive(),
|
mainView: (_) => Archive(),
|
||||||
emptyView: (_) => EmptyPage(),
|
emptyView: (_) => EmptyPage(),
|
||||||
);
|
);
|
||||||
case 'discover':
|
|
||||||
return ViewData(
|
|
||||||
mainView: (_) =>
|
|
||||||
DiscoverPage(alias: parts.length == 3 ? parts[2] : null),
|
|
||||||
emptyView: (_) => EmptyPage(),
|
|
||||||
);
|
|
||||||
case 'logs':
|
case 'logs':
|
||||||
return ViewData(
|
return ViewData(
|
||||||
mainView: (_) => LogViewer(),
|
mainView: (_) => LogViewer(),
|
||||||
);
|
);
|
||||||
case 'newgroup':
|
case 'newgroup':
|
||||||
return ViewData(
|
return ViewData(
|
||||||
leftView: (_) => ChatList(),
|
leftView: (_) => HomeView(),
|
||||||
mainView: (_) => NewGroup(),
|
mainView: (_) => NewGroup(),
|
||||||
);
|
);
|
||||||
case 'newprivatechat':
|
case 'newprivatechat':
|
||||||
return ViewData(
|
return ViewData(
|
||||||
leftView: (_) => ChatList(),
|
leftView: (_) => HomeView(),
|
||||||
mainView: (_) => NewPrivateChat(),
|
mainView: (_) => NewPrivateChat(),
|
||||||
);
|
);
|
||||||
|
case 'newstatus':
|
||||||
|
return ViewData(
|
||||||
|
leftView: (_) => HomeView(),
|
||||||
|
mainView: (_) => SetStatusView(initialText: settings.arguments),
|
||||||
|
);
|
||||||
case 'settings':
|
case 'settings':
|
||||||
if (parts.length == 3) {
|
if (parts.length == 3) {
|
||||||
final action = parts[2];
|
final action = parts[2];
|
||||||
|
@ -169,7 +168,9 @@ class FluffyRoutes {
|
||||||
case 'ignore':
|
case 'ignore':
|
||||||
return ViewData(
|
return ViewData(
|
||||||
leftView: (_) => Settings(),
|
leftView: (_) => Settings(),
|
||||||
mainView: (_) => SettingsIgnoreList(),
|
mainView: (_) => SettingsIgnoreList(
|
||||||
|
initialUserId: settings.arguments,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
case 'notifications':
|
case 'notifications':
|
||||||
return ViewData(
|
return ViewData(
|
||||||
|
|
|
@ -1332,8 +1332,36 @@
|
||||||
"username": {}
|
"username": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ignore": "Ignore",
|
||||||
|
"@ignore": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"status": "Status",
|
||||||
|
"@status": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"messages": "Messages",
|
||||||
|
"@messages": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"groups": "Groups",
|
||||||
|
"@groups": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"discover": "Discover",
|
||||||
|
"@discover": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"search": "Search",
|
||||||
|
"@search": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
"howOffensiveIsThisContent": "How offensive is this content?",
|
"howOffensiveIsThisContent": "How offensive is this content?",
|
||||||
"@howOffensiveIsThisContent": {
|
"@howOffensiveIsThisContent": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
|
19
lib/utils/status.dart
Normal file
19
lib/utils/status.dart
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
class Status {
|
||||||
|
static const String namespace = 'im.fluffychat.statuses';
|
||||||
|
final String senderId;
|
||||||
|
final String message;
|
||||||
|
final DateTime dateTime;
|
||||||
|
|
||||||
|
Status(this.senderId, this.message, this.dateTime);
|
||||||
|
|
||||||
|
Status.fromJson(Map<String, dynamic> json)
|
||||||
|
: senderId = json['sender_id'],
|
||||||
|
message = json['message'],
|
||||||
|
dateTime = DateTime.fromMillisecondsSinceEpoch(json['date_time']);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||||
|
'sender_id': senderId,
|
||||||
|
'message': message,
|
||||||
|
'date_time': dateTime.millisecondsSinceEpoch,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,341 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
|
||||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
|
||||||
import 'package:fluffychat/components/connection_status_header.dart';
|
|
||||||
import 'package:fluffychat/components/default_app_bar_search_field.dart';
|
|
||||||
import 'package:fluffychat/components/default_drawer.dart';
|
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
|
||||||
import 'package:fluffychat/app_config.dart';
|
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
|
||||||
|
|
||||||
import '../components/list_items/chat_list_item.dart';
|
|
||||||
import '../components/matrix.dart';
|
|
||||||
import '../utils/matrix_file_extension.dart';
|
|
||||||
import '../utils/url_launcher.dart';
|
|
||||||
|
|
||||||
enum SelectMode { normal, share, select }
|
|
||||||
|
|
||||||
class ChatList extends StatefulWidget {
|
|
||||||
final String activeChat;
|
|
||||||
|
|
||||||
const ChatList({this.activeChat, Key key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_ChatListState createState() => _ChatListState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ChatListState extends State<ChatList> {
|
|
||||||
bool get searchMode => searchController.text?.isNotEmpty ?? false;
|
|
||||||
final TextEditingController searchController = TextEditingController();
|
|
||||||
final _selectedRoomIds = <String>{};
|
|
||||||
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
|
||||||
bool _scrolledToTop = true;
|
|
||||||
|
|
||||||
void _toggleSelection(String roomId) =>
|
|
||||||
setState(() => _selectedRoomIds.contains(roomId)
|
|
||||||
? _selectedRoomIds.remove(roomId)
|
|
||||||
: _selectedRoomIds.add(roomId));
|
|
||||||
|
|
||||||
Future<void> waitForFirstSync(BuildContext context) async {
|
|
||||||
var client = Matrix.of(context).client;
|
|
||||||
if (client.prevBatch?.isEmpty ?? true) {
|
|
||||||
await client.onFirstSync.stream.first;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_scrollController.addListener(() async {
|
|
||||||
if (_scrollController.position.pixels > 0 && _scrolledToTop) {
|
|
||||||
setState(() => _scrolledToTop = false);
|
|
||||||
} else if (_scrollController.position.pixels == 0 && !_scrolledToTop) {
|
|
||||||
setState(() => _scrolledToTop = true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_initReceiveSharingIntent();
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamSubscription _intentDataStreamSubscription;
|
|
||||||
|
|
||||||
StreamSubscription _intentFileStreamSubscription;
|
|
||||||
|
|
||||||
void _processIncomingSharedFiles(List<SharedMediaFile> files) {
|
|
||||||
if (files?.isEmpty ?? true) return;
|
|
||||||
AdaptivePageLayout.of(context).popUntilIsFirst();
|
|
||||||
final file = File(files.first.path);
|
|
||||||
|
|
||||||
Matrix.of(context).shareContent = {
|
|
||||||
'msgtype': 'chat.fluffy.shared_file',
|
|
||||||
'file': MatrixFile(
|
|
||||||
bytes: file.readAsBytesSync(),
|
|
||||||
name: file.path,
|
|
||||||
).detectFileType,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void _processIncomingSharedText(String text) {
|
|
||||||
if (text == null) return;
|
|
||||||
AdaptivePageLayout.of(context).popUntilIsFirst();
|
|
||||||
if (text.toLowerCase().startsWith(AppConfig.inviteLinkPrefix) ||
|
|
||||||
(text.toLowerCase().startsWith(AppConfig.schemePrefix) &&
|
|
||||||
!RegExp(r'\s').hasMatch(text))) {
|
|
||||||
UrlLauncher(context, text).openMatrixToUrl();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Matrix.of(context).shareContent = {
|
|
||||||
'msgtype': 'm.text',
|
|
||||||
'body': text,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void _initReceiveSharingIntent() {
|
|
||||||
if (!PlatformInfos.isMobile) return;
|
|
||||||
|
|
||||||
// For sharing images coming from outside the app while the app is in the memory
|
|
||||||
_intentFileStreamSubscription = ReceiveSharingIntent.getMediaStream()
|
|
||||||
.listen(_processIncomingSharedFiles, onError: print);
|
|
||||||
|
|
||||||
// For sharing images coming from outside the app while the app is closed
|
|
||||||
ReceiveSharingIntent.getInitialMedia().then(_processIncomingSharedFiles);
|
|
||||||
|
|
||||||
// For sharing or opening urls/text coming from outside the app while the app is in the memory
|
|
||||||
_intentDataStreamSubscription = ReceiveSharingIntent.getTextStream()
|
|
||||||
.listen(_processIncomingSharedText, onError: print);
|
|
||||||
|
|
||||||
// For sharing or opening urls/text coming from outside the app while the app is closed
|
|
||||||
ReceiveSharingIntent.getInitialText().then(_processIncomingSharedText);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_intentDataStreamSubscription?.cancel();
|
|
||||||
_intentFileStreamSubscription?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _toggleUnread(BuildContext context) {
|
|
||||||
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
|
|
||||||
return showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () => room.setUnread(!room.isUnread),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _toggleFavouriteRoom(BuildContext context) {
|
|
||||||
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
|
|
||||||
return showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () => room.setFavourite(!room.isFavourite),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _toggleMuted(BuildContext context) {
|
|
||||||
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
|
|
||||||
return showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () => room.setPushRuleState(
|
|
||||||
room.pushRuleState == PushRuleState.notify
|
|
||||||
? PushRuleState.mentions_only
|
|
||||||
: PushRuleState.notify),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _archiveAction(BuildContext context) async {
|
|
||||||
final confirmed = await showOkCancelAlertDialog(
|
|
||||||
context: context,
|
|
||||||
title: L10n.of(context).areYouSure,
|
|
||||||
) ==
|
|
||||||
OkCancelResult.ok;
|
|
||||||
if (!confirmed) return;
|
|
||||||
await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () => _archiveSelectedRooms(context),
|
|
||||||
);
|
|
||||||
setState(() => null);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _archiveSelectedRooms(BuildContext context) async {
|
|
||||||
final client = Matrix.of(context).client;
|
|
||||||
while (_selectedRoomIds.isNotEmpty) {
|
|
||||||
final roomId = _selectedRoomIds.first;
|
|
||||||
await client.getRoomById(roomId).leave();
|
|
||||||
_selectedRoomIds.remove(roomId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return StreamBuilder(
|
|
||||||
stream: Matrix.of(context).onShareContentChanged.stream,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final selectMode = Matrix.of(context).shareContent == null
|
|
||||||
? _selectedRoomIds.isEmpty
|
|
||||||
? SelectMode.normal
|
|
||||||
: SelectMode.select
|
|
||||||
: SelectMode.share;
|
|
||||||
if (selectMode == SelectMode.share) {
|
|
||||||
_selectedRoomIds.clear();
|
|
||||||
}
|
|
||||||
Room selectedRoom;
|
|
||||||
if (_selectedRoomIds.length == 1) {
|
|
||||||
selectedRoom =
|
|
||||||
Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
|
|
||||||
}
|
|
||||||
return Scaffold(
|
|
||||||
drawer: selectMode != SelectMode.normal ? null : DefaultDrawer(),
|
|
||||||
appBar: AppBar(
|
|
||||||
centerTitle: false,
|
|
||||||
elevation: _scrolledToTop ? 0 : null,
|
|
||||||
leading: selectMode == SelectMode.share
|
|
||||||
? IconButton(
|
|
||||||
icon: Icon(Icons.close),
|
|
||||||
onPressed: () => Matrix.of(context).shareContent = null,
|
|
||||||
)
|
|
||||||
: selectMode == SelectMode.select
|
|
||||||
? IconButton(
|
|
||||||
icon: Icon(Icons.close),
|
|
||||||
onPressed: () => setState(_selectedRoomIds.clear),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
titleSpacing: 0,
|
|
||||||
actions: selectMode != SelectMode.select
|
|
||||||
? null
|
|
||||||
: [
|
|
||||||
if (_selectedRoomIds.length == 1)
|
|
||||||
IconButton(
|
|
||||||
tooltip: L10n.of(context).toggleUnread,
|
|
||||||
icon: Icon(selectedRoom.isUnread
|
|
||||||
? Icons.mark_chat_read_outlined
|
|
||||||
: Icons.mark_chat_unread_outlined),
|
|
||||||
onPressed: () => _toggleUnread(context),
|
|
||||||
),
|
|
||||||
if (_selectedRoomIds.length == 1)
|
|
||||||
IconButton(
|
|
||||||
tooltip: L10n.of(context).toggleFavorite,
|
|
||||||
icon: Icon(Icons.push_pin_outlined),
|
|
||||||
onPressed: () => _toggleFavouriteRoom(context),
|
|
||||||
),
|
|
||||||
if (_selectedRoomIds.length == 1)
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
selectedRoom.pushRuleState == PushRuleState.notify
|
|
||||||
? Icons.notifications_off_outlined
|
|
||||||
: Icons.notifications_outlined),
|
|
||||||
tooltip: L10n.of(context).toggleMuted,
|
|
||||||
onPressed: () => _toggleMuted(context),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.archive_outlined),
|
|
||||||
tooltip: L10n.of(context).archive,
|
|
||||||
onPressed: () => _archiveAction(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
title: selectMode == SelectMode.share
|
|
||||||
? Text(L10n.of(context).share)
|
|
||||||
: selectMode == SelectMode.select
|
|
||||||
? Text(_selectedRoomIds.length.toString())
|
|
||||||
: DefaultAppBarSearchField(
|
|
||||||
searchController: searchController,
|
|
||||||
hintText: L10n.of(context).searchForAChat,
|
|
||||||
onChanged: (_) => setState(() => null),
|
|
||||||
suffix: Icon(Icons.search_outlined),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
floatingActionButton:
|
|
||||||
AdaptivePageLayout.of(context).columnMode(context)
|
|
||||||
? null
|
|
||||||
: FloatingActionButton(
|
|
||||||
child: Icon(Icons.add_outlined),
|
|
||||||
onPressed: () => AdaptivePageLayout.of(context)
|
|
||||||
.pushNamedAndRemoveUntilIsFirst('/newprivatechat'),
|
|
||||||
),
|
|
||||||
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: waitForFirstSync(context),
|
|
||||||
builder: (BuildContext context, snapshot) {
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
var rooms = List<Room>.from(
|
|
||||||
Matrix.of(context).client.rooms);
|
|
||||||
rooms.removeWhere((Room room) =>
|
|
||||||
room.lastEvent == null ||
|
|
||||||
(searchMode &&
|
|
||||||
!room.displayname.toLowerCase().contains(
|
|
||||||
searchController.text.toLowerCase() ??
|
|
||||||
'')));
|
|
||||||
if (rooms.isEmpty && (!searchMode)) {
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
Icon(
|
|
||||||
searchMode
|
|
||||||
? Icons.search_outlined
|
|
||||||
: Icons.maps_ugc_outlined,
|
|
||||||
size: 80,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
searchMode
|
|
||||||
? L10n.of(context).noRoomsFound
|
|
||||||
: L10n.of(context).startYourFirstChat,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final totalCount = rooms.length;
|
|
||||||
return ListView.builder(
|
|
||||||
controller: _scrollController,
|
|
||||||
itemCount: totalCount,
|
|
||||||
itemBuilder: (BuildContext context, int i) =>
|
|
||||||
ChatListItem(
|
|
||||||
rooms[i],
|
|
||||||
selected:
|
|
||||||
_selectedRoomIds.contains(rooms[i].id),
|
|
||||||
onTap: selectMode == SelectMode.select
|
|
||||||
? () => _toggleSelection(rooms[i].id)
|
|
||||||
: null,
|
|
||||||
onLongPress: selectMode != SelectMode.share
|
|
||||||
? () => _toggleSelection(rooms[i].id)
|
|
||||||
: null,
|
|
||||||
activeChat: widget.activeChat == rooms[i].id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,238 +0,0 @@
|
||||||
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/components/avatar.dart';
|
|
||||||
import 'package:fluffychat/components/default_app_bar_search_field.dart';
|
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
|
||||||
import 'package:fluffychat/components/matrix.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|
||||||
|
|
||||||
class DiscoverPage extends StatefulWidget {
|
|
||||||
final String alias;
|
|
||||||
|
|
||||||
const DiscoverPage({Key key, this.alias}) : super(key: key);
|
|
||||||
@override
|
|
||||||
_DiscoverPageState createState() => _DiscoverPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DiscoverPageState extends State<DiscoverPage> {
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
|
||||||
bool _scrolledToTop = true;
|
|
||||||
Future<PublicRoomsResponse> _publicRoomsResponse;
|
|
||||||
Timer _coolDown;
|
|
||||||
String _server;
|
|
||||||
String _genericSearchTerm;
|
|
||||||
|
|
||||||
void _search(BuildContext context, String query) async {
|
|
||||||
_coolDown?.cancel();
|
|
||||||
_coolDown = Timer(
|
|
||||||
Duration(milliseconds: 500),
|
|
||||||
() => setState(() {
|
|
||||||
_genericSearchTerm = query;
|
|
||||||
_publicRoomsResponse = null;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setServer(BuildContext context) async {
|
|
||||||
final newServer = await showTextInputDialog(
|
|
||||||
title: L10n.of(context).changeTheHomeserver,
|
|
||||||
context: context,
|
|
||||||
textFields: [
|
|
||||||
DialogTextField(
|
|
||||||
hintText: Matrix.of(context).client.homeserver.toString(),
|
|
||||||
initialText: _server,
|
|
||||||
keyboardType: TextInputType.url,
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
if (newServer == null) return;
|
|
||||||
setState(() {
|
|
||||||
_server = newServer.single;
|
|
||||||
_publicRoomsResponse = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> _joinRoomAndWait(
|
|
||||||
BuildContext context,
|
|
||||||
String roomId,
|
|
||||||
String alias,
|
|
||||||
) async {
|
|
||||||
if (Matrix.of(context).client.getRoomById(roomId) != null) {
|
|
||||||
return roomId;
|
|
||||||
}
|
|
||||||
final newRoomId = await Matrix.of(context)
|
|
||||||
.client
|
|
||||||
.joinRoomOrAlias(alias?.isNotEmpty ?? false ? alias : roomId);
|
|
||||||
await Matrix.of(context)
|
|
||||||
.client
|
|
||||||
.onRoomUpdate
|
|
||||||
.stream
|
|
||||||
.firstWhere((r) => r.id == newRoomId);
|
|
||||||
return newRoomId;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _joinGroupAction(BuildContext context, PublicRoom room) async {
|
|
||||||
if (await showOkCancelAlertDialog(
|
|
||||||
context: context,
|
|
||||||
okLabel: L10n.of(context).joinRoom,
|
|
||||||
title: '${room.name} (${room.numJoinedMembers ?? 0})',
|
|
||||||
message: room.topic ?? L10n.of(context).noDescription,
|
|
||||||
) ==
|
|
||||||
OkCancelResult.cancel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final success = await showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () => _joinRoomAndWait(
|
|
||||||
context,
|
|
||||||
room.roomId,
|
|
||||||
room.canonicalAlias ?? room.aliases.first,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (success.error == null) {
|
|
||||||
await AdaptivePageLayout.of(context)
|
|
||||||
.pushNamedAndRemoveUntilIsFirst('/rooms/${success.result}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_genericSearchTerm = widget.alias;
|
|
||||||
_scrollController.addListener(() async {
|
|
||||||
if (_scrollController.position.pixels > 0 && _scrolledToTop) {
|
|
||||||
setState(() => _scrolledToTop = false);
|
|
||||||
} else if (_scrollController.position.pixels == 0 && !_scrolledToTop) {
|
|
||||||
setState(() => _scrolledToTop = true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final server = _genericSearchTerm?.isValidMatrixId ?? false
|
|
||||||
? _genericSearchTerm.domain
|
|
||||||
: _server;
|
|
||||||
_publicRoomsResponse ??= Matrix.of(context)
|
|
||||||
.client
|
|
||||||
.searchPublicRooms(
|
|
||||||
server: server,
|
|
||||||
genericSearchTerm: _genericSearchTerm,
|
|
||||||
)
|
|
||||||
.catchError((error) {
|
|
||||||
if (widget.alias == null) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return PublicRoomsResponse.fromJson({
|
|
||||||
'chunk': [],
|
|
||||||
});
|
|
||||||
}).then((PublicRoomsResponse res) {
|
|
||||||
if (widget.alias != null &&
|
|
||||||
!res.chunk.any((room) =>
|
|
||||||
room.aliases.contains(widget.alias) ||
|
|
||||||
room.canonicalAlias == widget.alias)) {
|
|
||||||
// we have to tack on the original alias
|
|
||||||
res.chunk.add(PublicRoom.fromJson(<String, dynamic>{
|
|
||||||
'aliases': [widget.alias],
|
|
||||||
'name': widget.alias,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
leading: BackButton(),
|
|
||||||
titleSpacing: 0,
|
|
||||||
elevation: _scrolledToTop ? 0 : null,
|
|
||||||
title: DefaultAppBarSearchField(
|
|
||||||
onChanged: (text) => _search(context, text),
|
|
||||||
hintText: L10n.of(context).searchForAChat,
|
|
||||||
suffix: IconButton(
|
|
||||||
icon: Icon(Icons.edit_outlined),
|
|
||||||
onPressed: () => _setServer(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: FutureBuilder<PublicRoomsResponse>(
|
|
||||||
future: _publicRoomsResponse,
|
|
||||||
builder: (BuildContext context,
|
|
||||||
AsyncSnapshot<PublicRoomsResponse> snapshot) {
|
|
||||||
if (snapshot.hasError) {
|
|
||||||
return Center(child: Text(snapshot.error.toString()));
|
|
||||||
}
|
|
||||||
if (snapshot.connectionState != ConnectionState.done) {
|
|
||||||
return Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
final publicRoomsResponse = snapshot.data;
|
|
||||||
if (publicRoomsResponse.chunk.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
|
||||||
'No public groups found...',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return GridView.builder(
|
|
||||||
padding: EdgeInsets.all(12),
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 2,
|
|
||||||
childAspectRatio: 1,
|
|
||||||
crossAxisSpacing: 16,
|
|
||||||
mainAxisSpacing: 16,
|
|
||||||
),
|
|
||||||
controller: _scrollController,
|
|
||||||
itemCount: publicRoomsResponse.chunk.length,
|
|
||||||
itemBuilder: (BuildContext context, int i) => Material(
|
|
||||||
elevation: 2,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => _joinGroupAction(
|
|
||||||
context,
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
250
lib/views/home_view.dart
Normal file
250
lib/views/home_view.dart
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||||
|
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:fluffychat/views/home_view_parts/discover.dart';
|
||||||
|
import 'package:fluffychat/views/share_view.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:fluffychat/app_config.dart';
|
||||||
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:preload_page_view/preload_page_view.dart';
|
||||||
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
|
import '../components/matrix.dart';
|
||||||
|
import '../utils/matrix_file_extension.dart';
|
||||||
|
import '../utils/url_launcher.dart';
|
||||||
|
import 'home_view_parts/chat_list.dart';
|
||||||
|
import 'home_view_parts/status_list.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
|
||||||
|
enum SelectMode { normal, share, select }
|
||||||
|
|
||||||
|
class HomeView extends StatefulWidget {
|
||||||
|
final String activeChat;
|
||||||
|
|
||||||
|
const HomeView({this.activeChat, Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_HomeViewState createState() => _HomeViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeViewState extends State<HomeView> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_initReceiveSharingIntent();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentIndex = 1;
|
||||||
|
|
||||||
|
StreamSubscription _intentDataStreamSubscription;
|
||||||
|
|
||||||
|
StreamSubscription _intentFileStreamSubscription;
|
||||||
|
|
||||||
|
StreamSubscription _onShareContentChanged;
|
||||||
|
|
||||||
|
AppBar appBar;
|
||||||
|
|
||||||
|
final PreloadPageController _pageController =
|
||||||
|
PreloadPageController(initialPage: 1);
|
||||||
|
|
||||||
|
void _onShare(Map<String, dynamic> content) {
|
||||||
|
if (content != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
|
(_) => Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ShareView(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _processIncomingSharedFiles(List<SharedMediaFile> files) {
|
||||||
|
if (files?.isEmpty ?? true) return;
|
||||||
|
AdaptivePageLayout.of(context).popUntilIsFirst();
|
||||||
|
final file = File(files.first.path);
|
||||||
|
|
||||||
|
Matrix.of(context).shareContent = {
|
||||||
|
'msgtype': 'chat.fluffy.shared_file',
|
||||||
|
'file': MatrixFile(
|
||||||
|
bytes: file.readAsBytesSync(),
|
||||||
|
name: file.path,
|
||||||
|
).detectFileType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void _processIncomingSharedText(String text) {
|
||||||
|
if (text == null) return;
|
||||||
|
AdaptivePageLayout.of(context).popUntilIsFirst();
|
||||||
|
if (text.toLowerCase().startsWith(AppConfig.inviteLinkPrefix) ||
|
||||||
|
(text.toLowerCase().startsWith(AppConfig.schemePrefix) &&
|
||||||
|
!RegExp(r'\s').hasMatch(text))) {
|
||||||
|
UrlLauncher(context, text).openMatrixToUrl();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Matrix.of(context).shareContent = {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initReceiveSharingIntent() {
|
||||||
|
if (!PlatformInfos.isMobile) return;
|
||||||
|
|
||||||
|
// For sharing images coming from outside the app while the app is in the memory
|
||||||
|
_intentFileStreamSubscription = ReceiveSharingIntent.getMediaStream()
|
||||||
|
.listen(_processIncomingSharedFiles, onError: print);
|
||||||
|
|
||||||
|
// For sharing images coming from outside the app while the app is closed
|
||||||
|
ReceiveSharingIntent.getInitialMedia().then(_processIncomingSharedFiles);
|
||||||
|
|
||||||
|
// For sharing or opening urls/text coming from outside the app while the app is in the memory
|
||||||
|
_intentDataStreamSubscription = ReceiveSharingIntent.getTextStream()
|
||||||
|
.listen(_processIncomingSharedText, onError: print);
|
||||||
|
|
||||||
|
// For sharing or opening urls/text coming from outside the app while the app is closed
|
||||||
|
ReceiveSharingIntent.getInitialText().then(_processIncomingSharedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_intentDataStreamSubscription?.cancel();
|
||||||
|
_intentFileStreamSubscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _server;
|
||||||
|
|
||||||
|
void _setServer(BuildContext context) async {
|
||||||
|
final newServer = await showTextInputDialog(
|
||||||
|
title: L10n.of(context).changeTheHomeserver,
|
||||||
|
context: context,
|
||||||
|
textFields: [
|
||||||
|
DialogTextField(
|
||||||
|
prefixText: 'https://',
|
||||||
|
hintText: Matrix.of(context).client.homeserver.host,
|
||||||
|
initialText: _server,
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
if (newServer == null) return;
|
||||||
|
setState(() {
|
||||||
|
_server = newServer.single;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFabTab() {
|
||||||
|
switch (currentIndex) {
|
||||||
|
case 0:
|
||||||
|
AdaptivePageLayout.of(context)
|
||||||
|
.pushNamedAndRemoveUntilIsFirst('/newstatus');
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
AdaptivePageLayout.of(context)
|
||||||
|
.pushNamedAndRemoveUntilIsFirst('/newprivatechat');
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
AdaptivePageLayout.of(context)
|
||||||
|
.pushNamedAndRemoveUntilIsFirst('/newgroup');
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
_setServer(context);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
_onShareContentChanged ??=
|
||||||
|
Matrix.of(context).onShareContentChanged.stream.listen(_onShare);
|
||||||
|
IconData fabIcon;
|
||||||
|
switch (currentIndex) {
|
||||||
|
case 0:
|
||||||
|
fabIcon = Icons.edit_outlined;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
fabIcon = Icons.add_outlined;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
fabIcon = Icons.group_add_outlined;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
fabIcon = Icons.domain_outlined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: appBar ??
|
||||||
|
AppBar(
|
||||||
|
centerTitle: false,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.account_circle_outlined),
|
||||||
|
onPressed: () =>
|
||||||
|
AdaptivePageLayout.of(context).pushNamed('/settings'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
title: Text(AppConfig.applicationName)),
|
||||||
|
body: PreloadPageView(
|
||||||
|
controller: _pageController,
|
||||||
|
onPageChanged: (i) => setState(() => currentIndex = i),
|
||||||
|
children: [
|
||||||
|
StatusList(key: Key('StatusList')),
|
||||||
|
ChatList(
|
||||||
|
type: ChatListType.messages,
|
||||||
|
onCustomAppBar: (appBar) => setState(() => this.appBar = appBar),
|
||||||
|
),
|
||||||
|
ChatList(
|
||||||
|
type: ChatListType.groups,
|
||||||
|
onCustomAppBar: (appBar) => setState(() => this.appBar = appBar),
|
||||||
|
),
|
||||||
|
Discover(server: _server),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
child: Icon(fabIcon),
|
||||||
|
onPressed: _onFabTab,
|
||||||
|
),
|
||||||
|
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
|
||||||
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
|
unselectedItemColor: Colors.black,
|
||||||
|
currentIndex: currentIndex,
|
||||||
|
showSelectedLabels: true,
|
||||||
|
showUnselectedLabels: false,
|
||||||
|
type: BottomNavigationBarType.fixed,
|
||||||
|
elevation: 20,
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
onTap: (i) {
|
||||||
|
_pageController.animateToPage(
|
||||||
|
i,
|
||||||
|
duration: Duration(milliseconds: 200),
|
||||||
|
curve: Curves.bounceOut,
|
||||||
|
);
|
||||||
|
setState(() => currentIndex = i);
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
label: L10n.of(context).status,
|
||||||
|
icon: Icon(Icons.home_outlined),
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
label: L10n.of(context).messages,
|
||||||
|
icon: Icon(CupertinoIcons.chat_bubble_2),
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
label: L10n.of(context).groups,
|
||||||
|
icon: Icon(Icons.people_outline),
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
label: L10n.of(context).discover,
|
||||||
|
icon: Icon(CupertinoIcons.search_circle),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
246
lib/views/home_view_parts/chat_list.dart
Normal file
246
lib/views/home_view_parts/chat_list.dart
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:fluffychat/components/connection_status_header.dart';
|
||||||
|
import 'package:fluffychat/components/default_app_bar_search_field.dart';
|
||||||
|
import 'package:fluffychat/components/list_items/chat_list_item.dart';
|
||||||
|
import 'package:fluffychat/components/matrix.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
|
|
||||||
|
enum ChatListType { messages, groups, all }
|
||||||
|
|
||||||
|
enum SelectMode { normal, select }
|
||||||
|
|
||||||
|
class ChatList extends StatefulWidget {
|
||||||
|
final String activeChat;
|
||||||
|
final ChatListType type;
|
||||||
|
final void Function(AppBar appBar) onCustomAppBar;
|
||||||
|
|
||||||
|
const ChatList({
|
||||||
|
Key key,
|
||||||
|
this.activeChat,
|
||||||
|
@required this.type,
|
||||||
|
this.onCustomAppBar,
|
||||||
|
}) : super(key: key);
|
||||||
|
@override
|
||||||
|
_ChatListState createState() => _ChatListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatListState extends State<ChatList> {
|
||||||
|
bool get searchMode => searchController.text?.isNotEmpty ?? false;
|
||||||
|
final TextEditingController searchController = TextEditingController();
|
||||||
|
final _selectedRoomIds = <String>{};
|
||||||
|
|
||||||
|
void _toggleSelection(String roomId) {
|
||||||
|
setState(() => _selectedRoomIds.contains(roomId)
|
||||||
|
? _selectedRoomIds.remove(roomId)
|
||||||
|
: _selectedRoomIds.add(roomId));
|
||||||
|
widget.onCustomAppBar(
|
||||||
|
_selectedRoomIds.isEmpty
|
||||||
|
? null
|
||||||
|
: AppBar(
|
||||||
|
centerTitle: false,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.close_outlined),
|
||||||
|
onPressed: () {
|
||||||
|
_selectedRoomIds.clear();
|
||||||
|
widget.onCustomAppBar(null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
L10n.of(context)
|
||||||
|
.numberSelected(_selectedRoomIds.length.toString()),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
if (_selectedRoomIds.length == 1)
|
||||||
|
IconButton(
|
||||||
|
tooltip: L10n.of(context).toggleUnread,
|
||||||
|
icon: Icon(Matrix.of(context)
|
||||||
|
.client
|
||||||
|
.getRoomById(_selectedRoomIds.single)
|
||||||
|
.isUnread
|
||||||
|
? Icons.mark_chat_read_outlined
|
||||||
|
: Icons.mark_chat_unread_outlined),
|
||||||
|
onPressed: () => _toggleUnread(context),
|
||||||
|
),
|
||||||
|
if (_selectedRoomIds.length == 1)
|
||||||
|
IconButton(
|
||||||
|
tooltip: L10n.of(context).toggleFavorite,
|
||||||
|
icon: Icon(Icons.push_pin_outlined),
|
||||||
|
onPressed: () => _toggleFavouriteRoom(context),
|
||||||
|
),
|
||||||
|
if (_selectedRoomIds.length == 1)
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Matrix.of(context)
|
||||||
|
.client
|
||||||
|
.getRoomById(_selectedRoomIds.single)
|
||||||
|
.pushRuleState ==
|
||||||
|
PushRuleState.notify
|
||||||
|
? Icons.notifications_off_outlined
|
||||||
|
: Icons.notifications_outlined),
|
||||||
|
tooltip: L10n.of(context).toggleMuted,
|
||||||
|
onPressed: () => _toggleMuted(context),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.archive_outlined),
|
||||||
|
tooltip: L10n.of(context).archive,
|
||||||
|
onPressed: () => _archiveAction(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleUnread(BuildContext context) {
|
||||||
|
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
|
||||||
|
return showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () => room.setUnread(!room.isUnread),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleFavouriteRoom(BuildContext context) {
|
||||||
|
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
|
||||||
|
return showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () => room.setFavourite(!room.isFavourite),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleMuted(BuildContext context) {
|
||||||
|
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
|
||||||
|
return showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () => room.setPushRuleState(
|
||||||
|
room.pushRuleState == PushRuleState.notify
|
||||||
|
? PushRuleState.mentions_only
|
||||||
|
: PushRuleState.notify),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _archiveAction(BuildContext context) async {
|
||||||
|
final confirmed = await showOkCancelAlertDialog(
|
||||||
|
context: context,
|
||||||
|
title: L10n.of(context).areYouSure,
|
||||||
|
) ==
|
||||||
|
OkCancelResult.ok;
|
||||||
|
if (!confirmed) return;
|
||||||
|
await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () => _archiveSelectedRooms(context),
|
||||||
|
);
|
||||||
|
setState(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _archiveSelectedRooms(BuildContext context) async {
|
||||||
|
final client = Matrix.of(context).client;
|
||||||
|
while (_selectedRoomIds.isNotEmpty) {
|
||||||
|
final roomId = _selectedRoomIds.first;
|
||||||
|
await client.getRoomById(roomId).leave();
|
||||||
|
_toggleSelection(roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> waitForFirstSync(BuildContext context) async {
|
||||||
|
var client = Matrix.of(context).client;
|
||||||
|
if (client.prevBatch?.isEmpty ?? true) {
|
||||||
|
await client.onFirstSync.stream.first;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final selectMode =
|
||||||
|
_selectedRoomIds.isEmpty ? SelectMode.normal : SelectMode.select;
|
||||||
|
return Column(children: [
|
||||||
|
ConnectionStatusHeader(),
|
||||||
|
Expanded(
|
||||||
|
child: StreamBuilder(
|
||||||
|
stream: Matrix.of(context)
|
||||||
|
.client
|
||||||
|
.onSync
|
||||||
|
.stream
|
||||||
|
.where((s) => s.hasRoomUpdate),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
return FutureBuilder<void>(
|
||||||
|
future: waitForFirstSync(context),
|
||||||
|
builder: (BuildContext context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
var rooms =
|
||||||
|
List<Room>.from(Matrix.of(context).client.rooms);
|
||||||
|
rooms.removeWhere((room) =>
|
||||||
|
room.lastEvent == null ||
|
||||||
|
(searchMode &&
|
||||||
|
!room.displayname.toLowerCase().contains(
|
||||||
|
searchController.text.toLowerCase() ?? '')));
|
||||||
|
if (widget.type == ChatListType.messages) {
|
||||||
|
rooms.removeWhere((room) => !room.isDirectChat);
|
||||||
|
} else if (widget.type == ChatListType.groups) {
|
||||||
|
rooms.removeWhere((room) => room.isDirectChat);
|
||||||
|
}
|
||||||
|
if (rooms.isEmpty && (!searchMode)) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
searchMode
|
||||||
|
? Icons.search_outlined
|
||||||
|
: Icons.maps_ugc_outlined,
|
||||||
|
size: 80,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
searchMode
|
||||||
|
? L10n.of(context).noRoomsFound
|
||||||
|
: L10n.of(context).startYourFirstChat,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final totalCount = rooms.length;
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: totalCount + 1,
|
||||||
|
itemBuilder: (BuildContext context, int i) => i == 0
|
||||||
|
? Padding(
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
|
child: DefaultAppBarSearchField(
|
||||||
|
hintText: L10n.of(context).search,
|
||||||
|
prefixIcon: Icon(Icons.search_outlined),
|
||||||
|
searchController: searchController,
|
||||||
|
onChanged: (_) => setState(() => null),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ChatListItem(
|
||||||
|
rooms[i - 1],
|
||||||
|
selected:
|
||||||
|
_selectedRoomIds.contains(rooms[i - 1].id),
|
||||||
|
onTap: selectMode == SelectMode.select &&
|
||||||
|
widget.onCustomAppBar != null
|
||||||
|
? () => _toggleSelection(rooms[i - 1].id)
|
||||||
|
: null,
|
||||||
|
onLongPress: widget.onCustomAppBar != null
|
||||||
|
? () => _toggleSelection(rooms[i - 1].id)
|
||||||
|
: null,
|
||||||
|
activeChat: widget.activeChat == rooms[i - 1].id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
215
lib/views/home_view_parts/discover.dart
Normal file
215
lib/views/home_view_parts/discover.dart
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
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/components/avatar.dart';
|
||||||
|
import 'package:fluffychat/components/default_app_bar_search_field.dart';
|
||||||
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
|
import 'package:fluffychat/components/matrix.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
|
||||||
|
class Discover extends StatefulWidget {
|
||||||
|
final String alias;
|
||||||
|
|
||||||
|
final String server;
|
||||||
|
|
||||||
|
const Discover({
|
||||||
|
Key key,
|
||||||
|
this.alias,
|
||||||
|
this.server,
|
||||||
|
}) : super(key: key);
|
||||||
|
@override
|
||||||
|
_DiscoverState createState() => _DiscoverState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DiscoverState extends State<Discover> {
|
||||||
|
Future<PublicRoomsResponse> _publicRoomsResponse;
|
||||||
|
Timer _coolDown;
|
||||||
|
String _genericSearchTerm;
|
||||||
|
|
||||||
|
void _search(BuildContext context, String query) async {
|
||||||
|
_coolDown?.cancel();
|
||||||
|
_coolDown = Timer(
|
||||||
|
Duration(milliseconds: 500),
|
||||||
|
() => setState(() {
|
||||||
|
_genericSearchTerm = query;
|
||||||
|
_publicRoomsResponse = null;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _joinRoomAndWait(
|
||||||
|
BuildContext context,
|
||||||
|
String roomId,
|
||||||
|
String alias,
|
||||||
|
) async {
|
||||||
|
if (Matrix.of(context).client.getRoomById(roomId) != null) {
|
||||||
|
return roomId;
|
||||||
|
}
|
||||||
|
final newRoomId = await Matrix.of(context)
|
||||||
|
.client
|
||||||
|
.joinRoomOrAlias(alias?.isNotEmpty ?? false ? alias : roomId);
|
||||||
|
await Matrix.of(context)
|
||||||
|
.client
|
||||||
|
.onRoomUpdate
|
||||||
|
.stream
|
||||||
|
.firstWhere((r) => r.id == newRoomId);
|
||||||
|
return newRoomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _joinGroupAction(BuildContext context, PublicRoom room) async {
|
||||||
|
if (await showOkCancelAlertDialog(
|
||||||
|
context: context,
|
||||||
|
okLabel: L10n.of(context).joinRoom,
|
||||||
|
title: '${room.name} (${room.numJoinedMembers ?? 0})',
|
||||||
|
message: room.topic ?? L10n.of(context).noDescription,
|
||||||
|
) ==
|
||||||
|
OkCancelResult.cancel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final success = await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () => _joinRoomAndWait(
|
||||||
|
context,
|
||||||
|
room.roomId,
|
||||||
|
room.canonicalAlias ?? room.aliases.first,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (success.error == null) {
|
||||||
|
await AdaptivePageLayout.of(context)
|
||||||
|
.pushNamedAndRemoveUntilIsFirst('/rooms/${success.result}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_genericSearchTerm = widget.alias;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final server = _genericSearchTerm?.isValidMatrixId ?? false
|
||||||
|
? _genericSearchTerm.domain
|
||||||
|
: widget.server;
|
||||||
|
_publicRoomsResponse ??= Matrix.of(context)
|
||||||
|
.client
|
||||||
|
.searchPublicRooms(
|
||||||
|
server: server,
|
||||||
|
genericSearchTerm: _genericSearchTerm,
|
||||||
|
)
|
||||||
|
.catchError((error) {
|
||||||
|
if (widget.alias == null) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return PublicRoomsResponse.fromJson({
|
||||||
|
'chunk': [],
|
||||||
|
});
|
||||||
|
}).then((PublicRoomsResponse res) {
|
||||||
|
if (widget.alias != null &&
|
||||||
|
!res.chunk.any((room) =>
|
||||||
|
room.aliases.contains(widget.alias) ||
|
||||||
|
room.canonicalAlias == widget.alias)) {
|
||||||
|
// we have to tack on the original alias
|
||||||
|
res.chunk.add(PublicRoom.fromJson(<String, dynamic>{
|
||||||
|
'aliases': [widget.alias],
|
||||||
|
'name': widget.alias,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
return ListView(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
|
child: DefaultAppBarSearchField(
|
||||||
|
hintText: L10n.of(context).search,
|
||||||
|
prefixIcon: Icon(Icons.search_outlined),
|
||||||
|
onChanged: (t) => _search(context, t),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FutureBuilder<PublicRoomsResponse>(
|
||||||
|
future: _publicRoomsResponse,
|
||||||
|
builder: (BuildContext context,
|
||||||
|
AsyncSnapshot<PublicRoomsResponse> snapshot) {
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Center(child: Text(snapshot.error.toString()));
|
||||||
|
}
|
||||||
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
final publicRoomsResponse = snapshot.data;
|
||||||
|
if (publicRoomsResponse.chunk.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
'No public groups found...',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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: () => _joinGroupAction(
|
||||||
|
context,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
67
lib/views/home_view_parts/status_list.dart
Normal file
67
lib/views/home_view_parts/status_list.dart
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import 'package:fluffychat/components/list_items/status_list_tile.dart';
|
||||||
|
import 'package:fluffychat/components/matrix.dart';
|
||||||
|
import 'package:fluffychat/utils/status.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class StatusList extends StatefulWidget {
|
||||||
|
const StatusList({Key key}) : super(key: key);
|
||||||
|
@override
|
||||||
|
_StatusListState createState() => _StatusListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatusListState extends State<StatusList> {
|
||||||
|
bool _onlyContacts = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView(children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
RaisedButton(
|
||||||
|
elevation: _onlyContacts ? 7 : null,
|
||||||
|
color: !_onlyContacts ? null : Theme.of(context).primaryColor,
|
||||||
|
child: Text(
|
||||||
|
'Contacts',
|
||||||
|
style: TextStyle(color: _onlyContacts ? Colors.white : null),
|
||||||
|
),
|
||||||
|
onPressed: () => setState(() => _onlyContacts = true),
|
||||||
|
),
|
||||||
|
RaisedButton(
|
||||||
|
elevation: !_onlyContacts ? 7 : null,
|
||||||
|
color: _onlyContacts ? null : Theme.of(context).primaryColor,
|
||||||
|
child: Text(
|
||||||
|
'All',
|
||||||
|
style: TextStyle(color: !_onlyContacts ? Colors.white : null),
|
||||||
|
),
|
||||||
|
onPressed: () => setState(() => _onlyContacts = false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Divider(height: 1),
|
||||||
|
StreamBuilder<Object>(
|
||||||
|
stream: Matrix.of(context)
|
||||||
|
.client
|
||||||
|
.onAccountData
|
||||||
|
.stream
|
||||||
|
.where((a) => a.type == Status.namespace),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final statuses = Matrix.of(context).statuses.values.toList()
|
||||||
|
..sort((a, b) => b.dateTime.compareTo(a.dateTime));
|
||||||
|
if (_onlyContacts) {
|
||||||
|
final client = Matrix.of(context).client;
|
||||||
|
statuses.removeWhere(
|
||||||
|
(p) => client.getDirectChatFromUserId(p.senderId) == null);
|
||||||
|
}
|
||||||
|
return ListView.separated(
|
||||||
|
physics: NeverScrollableScrollPhysics(),
|
||||||
|
shrinkWrap: true,
|
||||||
|
padding: EdgeInsets.only(bottom: 24),
|
||||||
|
separatorBuilder: (_, __) => Divider(height: 1),
|
||||||
|
itemCount: statuses.length,
|
||||||
|
itemBuilder: (context, i) => StatusListTile(status: statuses[i]),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
166
lib/views/set_status_view.dart
Normal file
166
lib/views/set_status_view.dart
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||||
|
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:file_picker_cross/file_picker_cross.dart';
|
||||||
|
import 'package:fluffychat/components/matrix.dart';
|
||||||
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import '../utils/string_color.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
|
||||||
|
class SetStatusView extends StatefulWidget {
|
||||||
|
final String initialText;
|
||||||
|
|
||||||
|
const SetStatusView({Key key, this.initialText}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SetStatusViewState createState() => _SetStatusViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SetStatusViewState extends State<SetStatusView> {
|
||||||
|
Color _color;
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller.text = widget.initialText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setStatusAction(BuildContext context, [String message]) async {
|
||||||
|
final result = await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () => Matrix.of(context).client.sendPresence(
|
||||||
|
Matrix.of(context).client.userID,
|
||||||
|
PresenceType.online,
|
||||||
|
statusMsg: message ?? _controller.text,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (result.error == null) AdaptivePageLayout.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setCameraImageStatusAction(BuildContext context) async {
|
||||||
|
MatrixFile file;
|
||||||
|
if (PlatformInfos.isMobile) {
|
||||||
|
final result = await ImagePicker().getImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
imageQuality: 50,
|
||||||
|
maxWidth: 1600,
|
||||||
|
maxHeight: 1600);
|
||||||
|
if (result == null) return;
|
||||||
|
file = MatrixFile(
|
||||||
|
bytes: await result.readAsBytes(),
|
||||||
|
name: result.path,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final uploadResp = await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () => Matrix.of(context).client.upload(file.bytes, file.name),
|
||||||
|
);
|
||||||
|
if (uploadResp.error == null) {
|
||||||
|
return _setStatusAction(context, uploadResp.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setImageStatusAction(BuildContext context) async {
|
||||||
|
MatrixFile file;
|
||||||
|
if (PlatformInfos.isMobile) {
|
||||||
|
final result = await ImagePicker().getImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
imageQuality: 50,
|
||||||
|
maxWidth: 1600,
|
||||||
|
maxHeight: 1600);
|
||||||
|
if (result == null) return;
|
||||||
|
file = MatrixFile(
|
||||||
|
bytes: await result.readAsBytes(),
|
||||||
|
name: result.path,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final result =
|
||||||
|
await FilePickerCross.importFromStorage(type: FileTypeCross.image);
|
||||||
|
if (result == null) return;
|
||||||
|
file = MatrixFile(
|
||||||
|
bytes: result.toUint8List(),
|
||||||
|
name: result.fileName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final uploadResp = await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () => Matrix.of(context).client.upload(file.bytes, file.name),
|
||||||
|
);
|
||||||
|
if (uploadResp.error == null) {
|
||||||
|
return _setStatusAction(context, uploadResp.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
_color ??= Theme.of(context).primaryColor;
|
||||||
|
return Scaffold(
|
||||||
|
extendBodyBehindAppBar: true,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5),
|
||||||
|
title: Text(L10n.of(context).statusExampleMessage),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.info_outlined),
|
||||||
|
onPressed: () => showOkAlertDialog(
|
||||||
|
context: context,
|
||||||
|
title: L10n.of(context).setStatus,
|
||||||
|
message:
|
||||||
|
'Show your status to all users you share a room. Every status will replace the previous one. Be aware that statuses are public and therefore not end-to-end encrypted.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: AnimatedContainer(
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
color: _color,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: TextField(
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 10,
|
||||||
|
autofocus: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
controller: _controller,
|
||||||
|
onChanged: (s) => setState(() => _color = s.color),
|
||||||
|
style: TextStyle(fontSize: 40, color: Colors.white),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
filled: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (PlatformInfos.isMobile) ...{
|
||||||
|
FloatingActionButton(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Theme.of(context).primaryColor,
|
||||||
|
child: Icon(Icons.camera_alt_outlined),
|
||||||
|
onPressed: () => _setCameraImageStatusAction(context),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
},
|
||||||
|
FloatingActionButton(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Theme.of(context).primaryColor,
|
||||||
|
child: Icon(Icons.image_outlined),
|
||||||
|
onPressed: () => _setImageStatusAction(context),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
FloatingActionButton(
|
||||||
|
child: Icon(Icons.send_outlined),
|
||||||
|
onPressed: () => _setStatusAction(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,9 +6,26 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
|
||||||
import '../components/matrix.dart';
|
import '../components/matrix.dart';
|
||||||
|
|
||||||
class SettingsIgnoreList extends StatelessWidget {
|
class SettingsIgnoreList extends StatefulWidget {
|
||||||
|
final String initialUserId;
|
||||||
|
|
||||||
|
SettingsIgnoreList({Key key, this.initialUserId}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SettingsIgnoreListState createState() => _SettingsIgnoreListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsIgnoreListState extends State<SettingsIgnoreList> {
|
||||||
final TextEditingController _controller = TextEditingController();
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.initialUserId != null) {
|
||||||
|
_controller.text = widget.initialUserId.replaceAll('@', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _ignoreUser(BuildContext context) {
|
void _ignoreUser(BuildContext context) {
|
||||||
if (_controller.text.isEmpty) return;
|
if (_controller.text.isEmpty) return;
|
||||||
final userId = '@${_controller.text}';
|
final userId = '@${_controller.text}';
|
||||||
|
|
27
lib/views/share_view.dart
Normal file
27
lib/views/share_view.dart
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||||
|
import 'package:fluffychat/components/matrix.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
|
||||||
|
import 'home_view_parts/chat_list.dart';
|
||||||
|
|
||||||
|
class ShareView extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.close_outlined),
|
||||||
|
onPressed: () {
|
||||||
|
Matrix.of(context).shareContent = null;
|
||||||
|
AdaptivePageLayout.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(L10n.of(context).share),
|
||||||
|
),
|
||||||
|
body: ChatList(
|
||||||
|
type: ChatListType.all,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
14
pubspec.lock
14
pubspec.lock
|
@ -176,6 +176,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.16.2"
|
version: "0.16.2"
|
||||||
|
cupertino_icons:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cupertino_icons
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
dapackages:
|
dapackages:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
@ -865,6 +872,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0-nullsafety.2"
|
version: "1.5.0-nullsafety.2"
|
||||||
|
preload_page_view:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: preload_page_view
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.4"
|
||||||
process:
|
process:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -20,6 +20,7 @@ dependencies:
|
||||||
url: https://github.com/UnifiedPush/flutter-connector.git
|
url: https://github.com/UnifiedPush/flutter-connector.git
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
|
cupertino_icons: any
|
||||||
localstorage: ^3.0.6+9
|
localstorage: ^3.0.6+9
|
||||||
file_picker_cross: 4.2.2
|
file_picker_cross: 4.2.2
|
||||||
image_picker: ^0.6.7+21
|
image_picker: ^0.6.7+21
|
||||||
|
@ -30,6 +31,7 @@ dependencies:
|
||||||
adaptive_page_layout: ^0.1.6
|
adaptive_page_layout: ^0.1.6
|
||||||
provider: ^4.3.3
|
provider: ^4.3.3
|
||||||
adaptive_theme: ^1.1.0
|
adaptive_theme: ^1.1.0
|
||||||
|
preload_page_view: ^0.1.4
|
||||||
# desktop_notifications: ^0.0.0-dev.4 // Currently blocked by: https://github.com/canonical/desktop_notifications.dart/issues/5
|
# desktop_notifications: ^0.0.0-dev.4 // Currently blocked by: https://github.com/canonical/desktop_notifications.dart/issues/5
|
||||||
matrix_link_text: ^0.3.2
|
matrix_link_text: ^0.3.2
|
||||||
path_provider: ^1.6.27
|
path_provider: ^1.6.27
|
||||||
|
|
Loading…
Reference in a new issue