mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-20 11:09:25 +01:00
Merge branch 'commands-ux' into 'main'
/commands input suggestions + missing dialog See merge request famedly/fluffychat!424
This commit is contained in:
commit
8fcdd4236a
@ -16,6 +16,88 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
|
"commandHintSend" : "Send text",
|
||||||
|
"@commandHintSend": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /send"
|
||||||
|
},
|
||||||
|
"commandHintMe" : "Describe yourself",
|
||||||
|
"@commandHintMe": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /me"
|
||||||
|
},
|
||||||
|
"commandHintPlain" : "Send unformatted text",
|
||||||
|
"@commandHintPlain": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /plain"
|
||||||
|
},
|
||||||
|
"commandHintHtml" : "Send HTML-formatted text",
|
||||||
|
"@commandHintHtml": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /html"
|
||||||
|
},
|
||||||
|
"commandHintReact" : "Send reply as a reaction",
|
||||||
|
"@commandHintReact": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /react"
|
||||||
|
},
|
||||||
|
"commandHintJoin" : "Join the given room",
|
||||||
|
"@commandHintJoin": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /join"
|
||||||
|
},
|
||||||
|
"commandHintLeave" : "Leave this room",
|
||||||
|
"@commandHintLeave": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /leave"
|
||||||
|
},
|
||||||
|
"commandHintOp" : "Set the given user's power level (default: 50)",
|
||||||
|
"@commandHintOp": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /op"
|
||||||
|
},
|
||||||
|
"commandHintKick" : "Remove the given user from this room",
|
||||||
|
"@commandHintKick": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /kick"
|
||||||
|
},
|
||||||
|
"commandHintBan" : "Ban the given user from this room",
|
||||||
|
"@commandHintBan": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /ban"
|
||||||
|
},
|
||||||
|
"commandHintUnBan" : "Unban the given user from this room",
|
||||||
|
"@commandHintUnBan": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /unban"
|
||||||
|
},
|
||||||
|
"commandHintInvite" : "Invite the given user to this room",
|
||||||
|
"@commandHintInvite": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /invite"
|
||||||
|
},
|
||||||
|
"commandHintMyRoomNick" : "Set your display name for this room",
|
||||||
|
"@commandHintMyRoomNick": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /myroomnick"
|
||||||
|
},
|
||||||
|
"commandHintMyRoomAvatar" : "Set your picture for this room (by mxc-uri)",
|
||||||
|
"@commandHintMyRoomAvatar": {
|
||||||
|
"type": "text",
|
||||||
|
"description": "Usage hint for the command /myroomavatar"
|
||||||
|
},
|
||||||
|
"commandInvalid": "Command invalid",
|
||||||
|
"@commandInvalid": {
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"commandMissing": "{command} is not a command.",
|
||||||
|
"@commandMissing": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {
|
||||||
|
"command": {}
|
||||||
|
},
|
||||||
|
"description": "State that {command} is not a valid /command."
|
||||||
|
},
|
||||||
"editRoomAliases": "Edit room aliases",
|
"editRoomAliases": "Edit room aliases",
|
||||||
"@editRoomAliases": {
|
"@editRoomAliases": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@ -1784,6 +1866,10 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
|
"sendAsText": "Send as text",
|
||||||
|
"@sendAsText": {
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
"sendAudio": "Send audio",
|
"sendAudio": "Send audio",
|
||||||
"@sendAudio": {
|
"@sendAudio": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -215,10 +215,31 @@ class ChatController extends State<Chat> {
|
|||||||
|
|
||||||
TextEditingController sendController = TextEditingController();
|
TextEditingController sendController = TextEditingController();
|
||||||
|
|
||||||
void send() {
|
Future<void> send() async {
|
||||||
if (sendController.text.trim().isEmpty) return;
|
if (sendController.text.trim().isEmpty) return;
|
||||||
|
var parseCommands = true;
|
||||||
|
|
||||||
|
final commandMatch = RegExp(r'^\/(\w+)').firstMatch(sendController.text);
|
||||||
|
if (commandMatch != null &&
|
||||||
|
!room.client.commands.keys.contains(commandMatch[1].toLowerCase())) {
|
||||||
|
final l10n = L10n.of(context);
|
||||||
|
final dialogResult = await showOkCancelAlertDialog(
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: false,
|
||||||
|
title: l10n.commandInvalid,
|
||||||
|
message: l10n.commandMissing(commandMatch[0]),
|
||||||
|
okLabel: l10n.sendAsText,
|
||||||
|
cancelLabel: l10n.cancel,
|
||||||
|
);
|
||||||
|
if (dialogResult == null || dialogResult == OkCancelResult.cancel) return;
|
||||||
|
parseCommands = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore: unawaited_futures
|
||||||
room.sendTextEvent(sendController.text,
|
room.sendTextEvent(sendController.text,
|
||||||
inReplyTo: replyEvent, editEventId: editEvent?.eventId);
|
inReplyTo: replyEvent,
|
||||||
|
editEventId: editEvent?.eventId,
|
||||||
|
parseCommands: parseCommands);
|
||||||
sendController.value = TextEditingValue(
|
sendController.value = TextEditingValue(
|
||||||
text: pendingText,
|
text: pendingText,
|
||||||
selection: TextSelection.collapsed(offset: 0),
|
selection: TextSelection.collapsed(offset: 0),
|
||||||
|
@ -2,6 +2,7 @@ import 'package:fluffychat/utils/platform_infos.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
@ -41,9 +42,24 @@ class InputBar extends StatelessWidget {
|
|||||||
final searchText =
|
final searchText =
|
||||||
controller.text.substring(0, controller.selection.baseOffset);
|
controller.text.substring(0, controller.selection.baseOffset);
|
||||||
final ret = <Map<String, String>>[];
|
final ret = <Map<String, String>>[];
|
||||||
|
const maxResults = 10;
|
||||||
|
|
||||||
|
final commandMatch = RegExp(r'^\/([\w]*)$').firstMatch(searchText);
|
||||||
|
if (commandMatch != null) {
|
||||||
|
final commandSearch = commandMatch[1].toLowerCase();
|
||||||
|
for (final command in room.client.commands.keys) {
|
||||||
|
if (command.contains(commandSearch)) {
|
||||||
|
ret.add({
|
||||||
|
'type': 'command',
|
||||||
|
'name': command,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret.length > maxResults) return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
final emojiMatch =
|
final emojiMatch =
|
||||||
RegExp(r'(?:\s|^):(?:([-\w]+)~)?([-\w]+)$').firstMatch(searchText);
|
RegExp(r'(?:\s|^):(?:([-\w]+)~)?([-\w]+)$').firstMatch(searchText);
|
||||||
final MAX_RESULTS = 10;
|
|
||||||
if (emojiMatch != null) {
|
if (emojiMatch != null) {
|
||||||
final packSearch = emojiMatch[1];
|
final packSearch = emojiMatch[1];
|
||||||
final emoteSearch = emojiMatch[2].toLowerCase();
|
final emoteSearch = emojiMatch[2].toLowerCase();
|
||||||
@ -59,11 +75,11 @@ class InputBar extends StatelessWidget {
|
|||||||
'mxc': emote.value,
|
'mxc': emote.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (ret.length > MAX_RESULTS) {
|
if (ret.length > maxResults) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ret.length > MAX_RESULTS) {
|
if (ret.length > maxResults) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,7 +93,7 @@ class InputBar extends StatelessWidget {
|
|||||||
'mxc': emote.value,
|
'mxc': emote.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (ret.length > MAX_RESULTS) {
|
if (ret.length > maxResults) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,7 +113,7 @@ class InputBar extends StatelessWidget {
|
|||||||
'avatar_url': user.avatarUrl?.toString(),
|
'avatar_url': user.avatarUrl?.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (ret.length > MAX_RESULTS) {
|
if (ret.length > maxResults) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,7 +149,7 @@ class InputBar extends StatelessWidget {
|
|||||||
'avatar_url': r.avatar?.toString(),
|
'avatar_url': r.avatar?.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (ret.length > MAX_RESULTS) {
|
if (ret.length > maxResults) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,13 +157,64 @@ class InputBar extends StatelessWidget {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _commandHint(L10n l10n, String command) {
|
||||||
|
switch (command) {
|
||||||
|
case 'send':
|
||||||
|
return l10n.commandHintSend;
|
||||||
|
case 'me':
|
||||||
|
return l10n.commandHintMe;
|
||||||
|
case 'plain':
|
||||||
|
return l10n.commandHintPlain;
|
||||||
|
case 'html':
|
||||||
|
return l10n.commandHintHtml;
|
||||||
|
case 'react':
|
||||||
|
return l10n.commandHintReact;
|
||||||
|
case 'join':
|
||||||
|
return l10n.commandHintJoin;
|
||||||
|
case 'leave':
|
||||||
|
return l10n.commandHintLeave;
|
||||||
|
case 'op':
|
||||||
|
return l10n.commandHintOp;
|
||||||
|
case 'kick':
|
||||||
|
return l10n.commandHintKick;
|
||||||
|
case 'ban':
|
||||||
|
return l10n.commandHintBan;
|
||||||
|
case 'unban':
|
||||||
|
return l10n.commandHintUnBan;
|
||||||
|
case 'invite':
|
||||||
|
return l10n.commandHintInvite;
|
||||||
|
case 'myroomnick':
|
||||||
|
return l10n.commandHintMyRoomNick;
|
||||||
|
case 'myroomavatar':
|
||||||
|
return l10n.commandHintMyRoomAvatar;
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildSuggestion(
|
Widget buildSuggestion(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Map<String, String> suggestion,
|
Map<String, String> suggestion,
|
||||||
Client client,
|
Client client,
|
||||||
) {
|
) {
|
||||||
|
const size = 30.0;
|
||||||
|
const padding = EdgeInsets.all(4.0);
|
||||||
|
if (suggestion['type'] == 'command') {
|
||||||
|
final command = suggestion['name'];
|
||||||
|
return Container(
|
||||||
|
padding: padding,
|
||||||
|
height: size + padding.bottom + padding.top,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('/' + command, style: TextStyle(fontFamily: 'monospace')),
|
||||||
|
Text(_commandHint(L10n.of(context), command),
|
||||||
|
style: Theme.of(context).textTheme.caption),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (suggestion['type'] == 'emote') {
|
if (suggestion['type'] == 'emote') {
|
||||||
final size = 30.0;
|
|
||||||
final ratio = MediaQuery.of(context).devicePixelRatio;
|
final ratio = MediaQuery.of(context).devicePixelRatio;
|
||||||
final url = Uri.parse(suggestion['mxc'] ?? '')?.getThumbnail(
|
final url = Uri.parse(suggestion['mxc'] ?? '')?.getThumbnail(
|
||||||
room.client,
|
room.client,
|
||||||
@ -157,7 +224,7 @@ class InputBar extends StatelessWidget {
|
|||||||
animated: true,
|
animated: true,
|
||||||
);
|
);
|
||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.all(4.0),
|
padding: padding,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@ -182,10 +249,9 @@ class InputBar extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (suggestion['type'] == 'user' || suggestion['type'] == 'room') {
|
if (suggestion['type'] == 'user' || suggestion['type'] == 'room') {
|
||||||
final size = 30.0;
|
|
||||||
final url = Uri.parse(suggestion['avatar_url'] ?? '');
|
final url = Uri.parse(suggestion['avatar_url'] ?? '');
|
||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.all(4.0),
|
padding: padding,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@ -212,6 +278,13 @@ class InputBar extends StatelessWidget {
|
|||||||
? ''
|
? ''
|
||||||
: controller.text.substring(controller.selection.baseOffset + 1);
|
: controller.text.substring(controller.selection.baseOffset + 1);
|
||||||
var insertText = '';
|
var insertText = '';
|
||||||
|
if (suggestion['type'] == 'command') {
|
||||||
|
insertText = suggestion['name'] + ' ';
|
||||||
|
startText = replaceText.replaceAllMapped(
|
||||||
|
RegExp(r'^(\/[\w]*)$'),
|
||||||
|
(Match m) => '/' + insertText,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (suggestion['type'] == 'emote') {
|
if (suggestion['type'] == 'emote') {
|
||||||
var isUnique = true;
|
var isUnique = true;
|
||||||
final insertEmote = suggestion['name'];
|
final insertEmote = suggestion['name'];
|
||||||
|
Loading…
Reference in New Issue
Block a user