Leave & delete channel

This commit is contained in:
LittleSheep 2024-04-26 23:25:56 +08:00
parent a02831644c
commit a761b80499
8 changed files with 222 additions and 65 deletions

View File

@ -22,6 +22,7 @@
"edit": "Edit", "edit": "Edit",
"apply": "Apply", "apply": "Apply",
"delete": "Delete", "delete": "Delete",
"exit": "Exit",
"action": "Action", "action": "Action",
"cancel": "Cancel", "cancel": "Cancel",
"report": "Report", "report": "Report",
@ -72,6 +73,8 @@
"chatChannelAliasLabel": "Channel Alias", "chatChannelAliasLabel": "Channel Alias",
"chatChannelNameLabel": "Channel Name", "chatChannelNameLabel": "Channel Name",
"chatChannelDescriptionLabel": "Channel Description", "chatChannelDescriptionLabel": "Channel Description",
"chatChannelLeaveConfirm": "Are you sure you want to leave this channel? Your message will be stored, but if you rejoin this channel later, you will lose your control of your previous messages.",
"chatChannelDeleteConfirm": "Are you sure you want to delete this channel? All messages in this channel will be gone forever. This operation cannot be revert!",
"chatMessagePlaceholder": "Write a message...", "chatMessagePlaceholder": "Write a message...",
"chatMessageEditNotify": "You are about editing a message.", "chatMessageEditNotify": "You are about editing a message.",
"chatMessageReplyNotify": "You are about replying a message.", "chatMessageReplyNotify": "You are about replying a message.",

View File

@ -24,6 +24,7 @@
"action": "操作", "action": "操作",
"apply": "应用", "apply": "应用",
"cancel": "取消", "cancel": "取消",
"exit": "离开",
"report": "举报", "report": "举报",
"reply": "回复", "reply": "回复",
"settings": "设置", "settings": "设置",
@ -72,6 +73,8 @@
"chatChannelAliasLabel": "频道别名", "chatChannelAliasLabel": "频道别名",
"chatChannelNameLabel": "频道名称", "chatChannelNameLabel": "频道名称",
"chatChannelDescriptionLabel": "频道简介", "chatChannelDescriptionLabel": "频道简介",
"chatChannelLeaveConfirm": "你确定你要离开这个频道吗?你在这个频道里的消息将被存储下来,但是当你重新加入本频道后你将会失去对你之前消息的权限。",
"chatChannelDeleteConfirm": "你确定你要删除这个频道吗?这个频道里的所有消息都将消失,并且不可被反转!",
"chatMessagePlaceholder": "发条消息……", "chatMessagePlaceholder": "发条消息……",
"chatMessageEditNotify": "你正在编辑信息中……", "chatMessageEditNotify": "你正在编辑信息中……",
"chatMessageReplyNotify": "你正在回复消息中……", "chatMessageReplyNotify": "你正在回复消息中……",

View File

@ -32,17 +32,19 @@ class _ChatScreenState extends State<ChatScreen> {
final http.Client _client = http.Client(); final http.Client _client = http.Client();
Future<void> fetchMetadata() async { Future<Channel> fetchMetadata() async {
var uri = getRequestUri('messaging', '/api/channels/${widget.alias}'); var uri = getRequestUri('messaging', '/api/channels/${widget.alias}');
var res = await _client.get(uri); var res = await _client.get(uri);
if (res.statusCode == 200) { if (res.statusCode == 200) {
final result = jsonDecode(utf8.decode(res.bodyBytes)); final result = jsonDecode(utf8.decode(res.bodyBytes));
setState(() => _channelMeta = Channel.fromJson(result)); setState(() => _channelMeta = Channel.fromJson(result));
return _channelMeta!;
} else { } else {
var message = utf8.decode(res.bodyBytes); var message = utf8.decode(res.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $message")), SnackBar(content: Text("Something went wrong... $message")),
); );
throw Exception(message);
} }
} }
@ -137,7 +139,15 @@ class _ChatScreenState extends State<ChatScreen> {
appBarActions: [ appBarActions: [
_channelMeta != null ? ChannelAction(channel: _channelMeta!, onUpdate: () => fetchMetadata()) : Container(), _channelMeta != null ? ChannelAction(channel: _channelMeta!, onUpdate: () => fetchMetadata()) : Container(),
], ],
child: ChatMaintainer( child: FutureBuilder(
future: fetchMetadata(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return const Center(child: CircularProgressIndicator());
}
return ChatMaintainer(
channel: snapshot.data!,
child: Column( child: Column(
children: [ children: [
Expanded( Expanded(
@ -188,6 +198,8 @@ class _ChatScreenState extends State<ChatScreen> {
onInsertMessage: (message) => addMessage(message), onInsertMessage: (message) => addMessage(message),
onUpdateMessage: (message) => updateMessage(message), onUpdateMessage: (message) => updateMessage(message),
onDeleteMessage: (message) => deleteMessage(message), onDeleteMessage: (message) => deleteMessage(message),
);
},
), ),
); );
} }

View File

@ -99,13 +99,17 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> {
), ),
title: Text(element.name), title: Text(element.name),
subtitle: Text(element.description), subtitle: Text(element.description),
onTap: () { onTap: () async {
router.pushNamed( final result = await router.pushNamed(
'chat.channel', 'chat.channel',
pathParameters: { pathParameters: {
'channel': element.alias, 'channel': element.alias,
}, },
); );
switch(result) {
case 'refresh':
fetchChannels();
}
}, },
); );
}, },

View File

@ -1,8 +1,10 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/widgets/chat/channel_deletion.dart';
import 'package:solian/widgets/indent_wrapper.dart'; import 'package:solian/widgets/indent_wrapper.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -16,7 +18,20 @@ class ChatManageScreen extends StatefulWidget {
} }
class _ChatManageScreenState extends State<ChatManageScreen> { class _ChatManageScreenState extends State<ChatManageScreen> {
bool isOwned = false; bool _isOwned = false;
void promptLeaveChannel() async {
final did = await showDialog(
context: context,
builder: (context) => ChannelDeletion(
channel: widget.channel,
isOwned: _isOwned,
),
);
if (did == true && router.canPop()) {
router.pop('disposed');
}
}
@override @override
void initState() { void initState() {
@ -27,7 +42,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
final prof = await auth.getProfiles(); final prof = await auth.getProfiles();
setState(() { setState(() {
isOwned = prof['id'] == widget.channel.account.externalId; _isOwned = prof['id'] == widget.channel.account.externalId;
}); });
}); });
} }
@ -41,7 +56,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
onTap: () async { onTap: () async {
router.pushNamed('chat.channel.editor', extra: widget.channel).then((did) { router.pushNamed('chat.channel.editor', extra: widget.channel).then((did) {
if (did == true) { if (did == true) {
if (router.canPop()) router.pop(true); if (router.canPop()) router.pop('refresh');
} }
}); });
}, },
@ -64,10 +79,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
child: Icon(Icons.tag, color: Colors.white), child: Icon(Icons.tag, color: Colors.white),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(widget.channel.name, style: Theme.of(context).textTheme.bodyLarge), Text(widget.channel.name, style: Theme.of(context).textTheme.bodyLarge),
Text(widget.channel.description, style: Theme.of(context).textTheme.bodySmall), Text(widget.channel.description, style: Theme.of(context).textTheme.bodySmall),
]) ]),
)
], ],
), ),
), ),
@ -91,7 +108,13 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
); );
}, },
), ),
...(isOwned ? authorizedItems : List.empty()), ...(_isOwned ? authorizedItems : List.empty()),
const Divider(thickness: 0.3),
ListTile(
leading: _isOwned ? const Icon(Icons.delete) : const Icon(Icons.exit_to_app),
title: Text(_isOwned ? AppLocalizations.of(context)!.delete : AppLocalizations.of(context)!.exit),
onTap: () => promptLeaveChannel(),
),
], ],
), ),
), ),

View File

@ -13,12 +13,18 @@ class ChannelAction extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IconButton( return IconButton(
onPressed: () { onPressed: () async {
router.pushNamed( final result = await router.pushNamed(
'chat.channel.manage', 'chat.channel.manage',
extra: channel, extra: channel,
pathParameters: {'channel': channel.alias}, pathParameters: {'channel': channel.alias},
); );
switch(result) {
case 'disposed':
if(router.canPop()) router.pop('refresh');
case 'refresh':
onUpdate();
}
}, },
focusNode: _focusNode, focusNode: _focusNode,
style: TextButton.styleFrom(shape: const CircleBorder()), style: TextButton.styleFrom(shape: const CircleBorder()),

View File

@ -0,0 +1,100 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:solian/models/channel.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/utils/service_url.dart';
class ChannelDeletion extends StatefulWidget {
final Channel channel;
final bool isOwned;
const ChannelDeletion({super.key, required this.channel, required this.isOwned});
@override
State<ChannelDeletion> createState() => _ChannelDeletionState();
}
class _ChannelDeletionState extends State<ChannelDeletion> {
bool _isSubmitting = false;
Future<void> deleteChannel() async {
setState(() => _isSubmitting = true);
final auth = context.read<AuthProvider>();
if (!await auth.isAuthorized()) {
setState(() => _isSubmitting = false);
return;
}
var res = await auth.client!.delete(
getRequestUri('messaging', '/api/channels/${widget.channel.id}'),
);
if (res.statusCode != 200) {
var message = utf8.decode(res.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $message")),
);
} else if (Navigator.canPop(context)) {
Navigator.pop(context, true);
}
setState(() => _isSubmitting = false);
}
Future<void> leaveChannel() async {
setState(() => _isSubmitting = true);
final auth = context.read<AuthProvider>();
if (!await auth.isAuthorized()) {
setState(() => _isSubmitting = false);
return;
}
var res = await auth.client!.post(
getRequestUri('messaging', '/api/channels/${widget.channel.alias}/leave'),
);
if (res.statusCode != 200) {
var message = utf8.decode(res.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $message")),
);
} else if (Navigator.canPop(context)) {
Navigator.pop(context, true);
}
setState(() => _isSubmitting = false);
}
@override
Widget build(BuildContext context) {
final content = widget.isOwned
? AppLocalizations.of(context)!.chatChannelDeleteConfirm
: AppLocalizations.of(context)!.chatChannelLeaveConfirm;
return AlertDialog(
title: Text(AppLocalizations.of(context)!.confirmation),
content: Text(content),
actions: <Widget>[
TextButton(
onPressed: _isSubmitting ? null : () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.confirmCancel),
),
TextButton(
onPressed: _isSubmitting
? null
: () {
if (widget.isOwned) {
deleteChannel();
} else {
leaveChannel();
}
},
child: Text(AppLocalizations.of(context)!.confirmOkay),
),
],
);
}
}

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/message.dart'; import 'package:solian/models/message.dart';
import 'package:solian/models/packet.dart'; import 'package:solian/models/packet.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
@ -10,6 +11,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class ChatMaintainer extends StatefulWidget { class ChatMaintainer extends StatefulWidget {
final Widget child; final Widget child;
final Channel channel;
final Function(Message val) onInsertMessage; final Function(Message val) onInsertMessage;
final Function(Message val) onUpdateMessage; final Function(Message val) onUpdateMessage;
final Function(Message val) onDeleteMessage; final Function(Message val) onDeleteMessage;
@ -17,6 +19,7 @@ class ChatMaintainer extends StatefulWidget {
const ChatMaintainer({ const ChatMaintainer({
super.key, super.key,
required this.child, required this.child,
required this.channel,
required this.onInsertMessage, required this.onInsertMessage,
required this.onUpdateMessage, required this.onUpdateMessage,
required this.onDeleteMessage, required this.onDeleteMessage,
@ -46,13 +49,16 @@ class _ChatMaintainerState extends State<ChatMaintainer> {
final result = NetworkPackage.fromJson(jsonDecode(event)); final result = NetworkPackage.fromJson(jsonDecode(event));
switch (result.method) { switch (result.method) {
case 'messages.new': case 'messages.new':
widget.onInsertMessage(Message.fromJson(result.payload!)); final payload = Message.fromJson(result.payload!);
if (payload.channelId == widget.channel.id) widget.onInsertMessage(payload);
break; break;
case 'messages.update': case 'messages.update':
widget.onUpdateMessage(Message.fromJson(result.payload!)); final payload = Message.fromJson(result.payload!);
if (payload.channelId == widget.channel.id) widget.onUpdateMessage(payload);
break; break;
case 'messages.burnt': case 'messages.burnt':
widget.onDeleteMessage(Message.fromJson(result.payload!)); final payload = Message.fromJson(result.payload!);
if (payload.channelId == widget.channel.id) widget.onDeleteMessage(payload);
break; break;
} }
}, },