FurryChat/lib/components/input_bar.dart

296 lines
9.6 KiB
Dart
Raw Normal View History

2020-05-15 13:28:23 +00:00
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:flutter_advanced_networkimage/provider.dart';
import 'avatar.dart';
2020-05-15 13:28:23 +00:00
class InputBar extends StatelessWidget {
final Room room;
final int minLines;
final int maxLines;
final TextInputType keyboardType;
final ValueChanged<String> onSubmitted;
final FocusNode focusNode;
final TextEditingController controller;
final InputDecoration decoration;
final ValueChanged<String> onChanged;
InputBar({
this.room,
this.minLines,
this.maxLines,
this.keyboardType,
this.onSubmitted,
this.focusNode,
this.controller,
this.decoration,
this.onChanged,
});
List<Map<String, String>> getSuggestions(String text) {
2020-05-15 19:50:44 +00:00
if (controller.selection.baseOffset != controller.selection.extentOffset ||
controller.selection.baseOffset < 0) {
2020-05-15 13:28:23 +00:00
return []; // no entries if there is selected text
}
2020-05-15 19:50:44 +00:00
final searchText =
controller.text.substring(0, controller.selection.baseOffset);
2020-05-15 13:28:23 +00:00
final ret = <Map<String, String>>[];
2020-05-15 19:50:44 +00:00
final emojiMatch =
RegExp(r'(?:\s|^):(?:([-\w]+)~)?([-\w]+)$').firstMatch(searchText);
final MAX_RESULTS = 10;
2020-05-15 13:28:23 +00:00
if (emojiMatch != null) {
final packSearch = emojiMatch[1];
final emoteSearch = emojiMatch[2].toLowerCase();
final emotePacks = room.emotePacks;
2020-05-15 13:28:23 +00:00
if (packSearch == null || packSearch.isEmpty) {
for (final pack in emotePacks.entries) {
for (final emote in pack.value.entries) {
if (emote.key.toLowerCase().contains(emoteSearch)) {
ret.add({
'type': 'emote',
'name': emote.key,
'pack': pack.key,
'mxc': emote.value,
});
}
if (ret.length > MAX_RESULTS) {
2020-05-15 13:28:23 +00:00
break;
}
}
if (ret.length > MAX_RESULTS) {
2020-05-15 13:28:23 +00:00
break;
}
}
} else if (emotePacks[packSearch] != null) {
for (final emote in emotePacks[packSearch].entries) {
if (emote.key.toLowerCase().contains(emoteSearch)) {
ret.add({
'type': 'emote',
'name': emote.key,
'pack': packSearch,
'mxc': emote.value,
});
}
if (ret.length > MAX_RESULTS) {
2020-05-15 13:28:23 +00:00
break;
}
}
}
}
final userMatch = RegExp(r'(?:\s|^)@([-\w]+)$').firstMatch(searchText);
if (userMatch != null) {
final userSearch = userMatch[1].toLowerCase();
for (final user in room.getParticipants()) {
2020-05-22 10:21:16 +00:00
if ((user.displayName != null &&
user.displayName.toLowerCase().contains(userSearch)) ||
user.id.split(':')[0].toLowerCase().contains(userSearch)) {
ret.add({
'type': 'user',
'mxid': user.id,
'displayname': user.displayName,
'avatar_url': user.avatarUrl?.toString(),
});
}
if (ret.length > MAX_RESULTS) {
break;
}
}
}
final roomMatch = RegExp(r'(?:\s|^)#([-\w]+)$').firstMatch(searchText);
if (roomMatch != null) {
final roomSearch = roomMatch[1].toLowerCase();
for (final r in room.client.rooms) {
final state = r.getState('m.room.canonical_alias');
2020-05-22 10:21:16 +00:00
if (state != null &&
((state.content['alias'] is String &&
state.content['alias']
.split(':')[0]
.toLowerCase()
.contains(roomSearch)) ||
(state.content['alt_aliases'] is List &&
state.content['alt_aliases'].any((l) =>
l is String &&
l.split(':')[0].toLowerCase().contains(roomSearch))) ||
(room.name != null &&
room.name.toLowerCase().contains(roomSearch)))) {
ret.add({
'type': 'room',
2020-05-22 10:21:16 +00:00
'mxid': (r.canonicalAlias != null && r.canonicalAlias.isNotEmpty)
? r.canonicalAlias
: r.id,
'displayname': r.displayname,
'avatar_url': r.avatar?.toString(),
});
}
if (ret.length > MAX_RESULTS) {
break;
}
}
}
2020-05-15 13:28:23 +00:00
return ret;
}
Widget buildSuggestion(BuildContext context, Map<String, String> suggestion) {
if (suggestion['type'] == 'emote') {
final size = 30.0;
final ratio = MediaQuery.of(context).devicePixelRatio;
final url = Uri.parse(suggestion['mxc'] ?? '')?.getThumbnail(
room.client,
width: size * ratio,
height: size * ratio,
method: ThumbnailMethod.scale,
);
return Container(
padding: EdgeInsets.all(4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Image(
2020-05-15 19:50:44 +00:00
image: AdvancedNetworkImage(url, useDiskCache: !kIsWeb),
2020-05-15 13:28:23 +00:00
width: size,
height: size,
),
SizedBox(width: 6),
Text(suggestion['name']),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Opacity(
opacity: 0.5,
child: Text(suggestion['pack']),
),
),
),
],
),
);
}
if (suggestion['type'] == 'user' || suggestion['type'] == 'room') {
final size = 30.0;
final url = Uri.parse(suggestion['avatar_url'] ?? '');
return Container(
padding: EdgeInsets.all(4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
2020-05-22 10:21:16 +00:00
Avatar(url, suggestion['displayname'] ?? suggestion['mxid'],
size: size),
SizedBox(width: 6),
Text(suggestion['displayname'] ?? suggestion['mxid']),
],
),
);
}
2020-05-15 13:28:23 +00:00
return Container();
}
void insertSuggestion(BuildContext context, Map<String, String> suggestion) {
final replaceText =
controller.text.substring(0, controller.selection.baseOffset);
var startText = '';
final afterText = replaceText == controller.text
2020-05-22 10:21:16 +00:00
? ''
: controller.text.substring(controller.selection.baseOffset + 1);
var insertText = '';
2020-05-15 13:28:23 +00:00
if (suggestion['type'] == 'emote') {
var isUnique = true;
final insertEmote = suggestion['name'];
final insertPack = suggestion['pack'];
final emotePacks = room.emotePacks;
2020-05-15 13:28:23 +00:00
for (final pack in emotePacks.entries) {
if (pack.key == insertPack) {
continue;
}
for (final emote in pack.value.entries) {
if (emote.key == insertEmote) {
isUnique = false;
break;
}
}
if (!isUnique) {
break;
}
}
2020-05-22 10:21:16 +00:00
insertText = (isUnique
? insertEmote
: ':${insertPack}~${insertEmote.substring(1)}') +
' ';
startText = replaceText.replaceAllMapped(
2020-05-15 13:28:23 +00:00
RegExp(r'(\s|^)(:(?:[-\w]+~)?[-\w]+)$'),
(Match m) => '${m[1]}${insertText}',
);
}
if (suggestion['type'] == 'user') {
insertText = suggestion['mxid'] + ' ';
startText = replaceText.replaceAllMapped(
RegExp(r'(\s|^)(@[-\w]+)$'),
(Match m) => '${m[1]}${insertText}',
);
}
if (suggestion['type'] == 'room') {
insertText = suggestion['mxid'] + ' ';
startText = replaceText.replaceAllMapped(
RegExp(r'(\s|^)(#[-\w]+)$'),
(Match m) => '${m[1]}${insertText}',
2020-05-15 13:28:23 +00:00
);
}
if (insertText.isNotEmpty && startText.isNotEmpty) {
2020-05-15 13:28:23 +00:00
controller.text = startText + afterText;
if (startText == insertText) {
2020-05-15 13:28:23 +00:00
// stupid fix for now
FocusScope.of(context).requestFocus(FocusNode());
Future.delayed(Duration(milliseconds: 1)).then((res) {
focusNode.requestFocus();
controller.selection = TextSelection(
baseOffset: startText.length,
extentOffset: startText.length,
);
});
} else {
controller.selection = TextSelection(
baseOffset: startText.length,
extentOffset: startText.length,
);
}
}
}
@override
Widget build(BuildContext context) {
return TypeAheadField<Map<String, String>>(
direction: AxisDirection.up,
hideOnEmpty: true,
hideOnLoading: true,
keepSuggestionsOnSuggestionSelected: true,
2020-05-15 19:50:44 +00:00
debounceDuration: Duration(
milliseconds:
50), // show suggestions after 50ms idle time (default is 300)
2020-05-15 13:28:23 +00:00
textFieldConfiguration: TextFieldConfiguration(
minLines: minLines,
maxLines: maxLines,
keyboardType: keyboardType,
2020-05-15 19:50:44 +00:00
onSubmitted: (text) {
// fix for library for now
2020-05-15 13:28:23 +00:00
onSubmitted(text);
},
focusNode: focusNode,
controller: controller,
decoration: decoration,
onChanged: (text) {
onChanged(text);
},
),
suggestionsCallback: getSuggestions,
itemBuilder: buildSuggestion,
2020-05-15 19:50:44 +00:00
onSuggestionSelected: (Map<String, String> suggestion) =>
insertSuggestion(context, suggestion),
2020-05-15 13:28:23 +00:00
errorBuilder: (BuildContext context, Object error) => Container(),
2020-05-15 19:50:44 +00:00
loadingBuilder: (BuildContext context) =>
Container(), // fix loading briefly flickering a dark box
noItemsFoundBuilder: (BuildContext context) =>
Container(), // fix loading briefly showing no suggestions
2020-05-15 13:28:23 +00:00
);
}
}