feat: Enhance emote experience

This commit is contained in:
Sorunome 2020-10-03 12:31:29 +02:00
parent 090795fa77
commit cafd639c24
No known key found for this signature in database
GPG key ID: B19471D07FC9BE9C
8 changed files with 252 additions and 70 deletions

View file

@ -1,4 +1,9 @@
# Version 0.19.0 - 2020-??-?? # Version 0.20.0 - 2020-??-??
### Features
- Add ability to enable / disable emotes globally
- Add ability to manage emote packs with different state keys
# Version 0.19.0 - 2020-09-21
### Features ### Features
- Implemented ignore list - Implemented ignore list
- Jump to events in timeline: When tapping on a reply and when tapping a matrix.to link - Jump to events in timeline: When tapping on a reply and when tapping a matrix.to link

View file

@ -237,22 +237,10 @@ class InputBar extends StatelessWidget {
} }
if (insertText.isNotEmpty && startText.isNotEmpty) { if (insertText.isNotEmpty && startText.isNotEmpty) {
controller.text = startText + afterText; controller.text = startText + afterText;
if (startText == insertText) {
// stupid fix for now
FocusScope.of(context).requestFocus(FocusNode());
Future.delayed(Duration(milliseconds: 1)).then((res) {
focusNode.requestFocus();
controller.selection = TextSelection( controller.selection = TextSelection(
baseOffset: startText.length, baseOffset: startText.length,
extentOffset: startText.length, extentOffset: startText.length,
); );
});
} else {
controller.selection = TextSelection(
baseOffset: startText.length,
extentOffset: startText.length,
);
}
} }
} }

View file

@ -529,11 +529,21 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"emotePacks": "Emote packs for room",
"@emotePacks": {
"type": "text",
"placeholders": {}
},
"emptyChat": "Empty chat", "emptyChat": "Empty chat",
"@emptyChat": { "@emptyChat": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"enableEmotesGlobally": "Enable emote pack globally",
"@enableEmotesGlobally": {
"type": "text",
"placeholders": {}
},
"enableEncryptionWarning": "You won't be able to disable the encryption anymore. Are you sure?", "enableEncryptionWarning": "You won't be able to disable the encryption anymore. Are you sure?",
"@enableEncryptionWarning": { "@enableEncryptionWarning": {
"type": "text", "type": "text",

View file

@ -19,6 +19,7 @@ import 'package:matrix_link_text/link_text.dart';
import 'package:memoryfilepicker/memoryfilepicker.dart'; import 'package:memoryfilepicker/memoryfilepicker.dart';
import './settings_emotes.dart'; import './settings_emotes.dart';
import './settings_multiple_emotes.dart';
import '../utils/url_launcher.dart'; import '../utils/url_launcher.dart';
class ChatDetails extends StatefulWidget { class ChatDetails extends StatefulWidget {
@ -285,13 +286,31 @@ class _ChatDetailsState extends State<ChatDetails> {
child: Icon(Icons.insert_emoticon), child: Icon(Icons.insert_emoticon),
), ),
title: Text(L10n.of(context).emoteSettings), title: Text(L10n.of(context).emoteSettings),
onTap: () async => onTap: () async {
// okay, we need to test if there are any emote state events other than the default one
// if so, we need to be directed to a selection screen for which pack we want to look at
// otherwise, we just open the normal one.
if ((widget.room.states
.states['im.ponies.room_emotes'] ??
<String, Event>{})
.keys
.any((String s) => s.isNotEmpty)) {
await Navigator.of(context).push(
AppRoute.defaultRoute(
context,
MultipleEmotesSettingsView(
room: widget.room),
),
);
} else {
await Navigator.of(context).push( await Navigator.of(context).push(
AppRoute.defaultRoute( AppRoute.defaultRoute(
context, context,
EmotesSettingsView(room: widget.room), EmotesSettingsView(room: widget.room),
), ),
), );
}
},
), ),
PopupMenuButton( PopupMenuButton(
child: ListTile( child: ListTile(

View file

@ -14,23 +14,25 @@ import 'chat_list.dart';
class EmotesSettingsView extends StatelessWidget { class EmotesSettingsView extends StatelessWidget {
final Room room; final Room room;
final String stateKey;
EmotesSettingsView({this.room}); EmotesSettingsView({this.room, this.stateKey});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AdaptivePageLayout( return AdaptivePageLayout(
primaryPage: FocusPage.SECOND, primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(), firstScaffold: ChatList(),
secondScaffold: EmotesSettings(room: room), secondScaffold: EmotesSettings(room: room, stateKey: stateKey),
); );
} }
} }
class EmotesSettings extends StatefulWidget { class EmotesSettings extends StatefulWidget {
final Room room; final Room room;
final String stateKey;
EmotesSettings({this.room}); EmotesSettings({this.room, this.stateKey});
@override @override
_EmotesSettingsState createState() => _EmotesSettingsState(); _EmotesSettingsState createState() => _EmotesSettingsState();
@ -59,21 +61,42 @@ class _EmotesSettingsState extends State<EmotesSettings> {
// be sure to preserve any data not in "short" // be sure to preserve any data not in "short"
Map<String, dynamic> content; Map<String, dynamic> content;
if (widget.room != null) { if (widget.room != null) {
content = widget.room.getState('im.ponies.room_emotes')?.content ?? content = widget.room
.getState('im.ponies.room_emotes', widget.stateKey ?? '')
?.content ??
<String, dynamic>{}; <String, dynamic>{};
} else { } else {
content = client.accountData['im.ponies.user_emotes']?.content ?? content = client.accountData['im.ponies.user_emotes']?.content ??
<String, dynamic>{}; <String, dynamic>{};
} }
debugPrint(content.toString()); debugPrint(content.toString());
content['short'] = <String, String>{}; if (!(content['emoticons'] is Map)) {
for (final emote in emotes) { content['emoticons'] = <String, dynamic>{};
content['short'][emote.emote] = emote.mxc;
} }
// add / update changed emotes
final allowedShortcodes = <String>{};
for (final emote in emotes) {
allowedShortcodes.add(emote.emote);
if (!(content['emoticons'][emote.emote] is Map)) {
content['emoticons'][emote.emote] = <String, dynamic>{};
}
content['emoticons'][emote.emote]['url'] = emote.mxc;
}
// remove emotes no more needed
// we make the iterator .toList() here so that we don't get into trouble modifying the very
// thing we are iterating over
for (final shortcode in content['emoticons'].keys.toList()) {
if (!allowedShortcodes.contains(shortcode)) {
content['emoticons'].remove(shortcode);
}
}
// remove the old "short" key
content.remove('short');
debugPrint(content.toString()); debugPrint(content.toString());
if (widget.room != null) { if (widget.room != null) {
await SimpleDialogs(context).tryRequestWithLoadingDialog( await SimpleDialogs(context).tryRequestWithLoadingDialog(
client.sendState(widget.room.id, 'im.ponies.room_emotes', content), client.sendState(widget.room.id, 'im.ponies.room_emotes', content,
widget.stateKey ?? ''),
); );
} else { } else {
await SimpleDialogs(context).tryRequestWithLoadingDialog( await SimpleDialogs(context).tryRequestWithLoadingDialog(
@ -82,6 +105,43 @@ class _EmotesSettingsState extends State<EmotesSettings> {
} }
} }
Future<void> _setIsGloballyActive(BuildContext context, bool active) async {
if (widget.room == null) {
return;
}
final client = Matrix.of(context).client;
final content = client.accountData['im.ponies.emote_rooms']?.content ??
<String, dynamic>{};
if (active) {
if (!(content['rooms'] is Map)) {
content['rooms'] = <String, dynamic>{};
}
if (!(content['rooms'][widget.room.id] is Map)) {
content['rooms'][widget.room.id] = <String, dynamic>{};
}
if (!(content['rooms'][widget.room.id][widget.stateKey ?? ''] is Map)) {
content['rooms'][widget.room.id]
[widget.stateKey ?? ''] = <String, dynamic>{};
}
} else if (content['rooms'] is Map &&
content['rooms'][widget.room.id] is Map) {
content['rooms'][widget.room.id].remove(widget.stateKey ?? '');
}
// and save
await SimpleDialogs(context).tryRequestWithLoadingDialog(
client.setAccountData(client.userID, 'im.ponies.emote_rooms', content),
);
}
bool isGloballyActive(Client client) =>
widget.room != null &&
client.accountData['im.ponies.emote_rooms']?.content is Map &&
client.accountData['im.ponies.emote_rooms'].content['rooms'] is Map &&
client.accountData['im.ponies.emote_rooms'].content['rooms']
[widget.room.id] is Map &&
client.accountData['im.ponies.emote_rooms'].content['rooms']
[widget.room.id][widget.stateKey ?? ''] is Map;
bool get readonly => widget.room == null bool get readonly => widget.room == null
? false ? false
: !(widget.room.canSendEvent('im.ponies.room_emotes')); : !(widget.room.canSendEvent('im.ponies.room_emotes'));
@ -93,18 +153,33 @@ class _EmotesSettingsState extends State<EmotesSettings> {
emotes = <_EmoteEntry>[]; emotes = <_EmoteEntry>[];
Map<String, dynamic> emoteSource; Map<String, dynamic> emoteSource;
if (widget.room != null) { if (widget.room != null) {
emoteSource = widget.room.getState('im.ponies.room_emotes')?.content; emoteSource = widget.room
.getState('im.ponies.room_emotes', widget.stateKey ?? '')
?.content;
} else { } else {
emoteSource = client.accountData['im.ponies.user_emotes']?.content; emoteSource = client.accountData['im.ponies.user_emotes']?.content;
} }
if (emoteSource != null && emoteSource['short'] is Map) { if (emoteSource != null) {
if (emoteSource['emoticons'] is Map) {
emoteSource['emoticons'].forEach((key, value) {
if (key is String &&
value is Map &&
value['url'] is String &&
value['url'].startsWith('mxc://')) {
emotes.add(_EmoteEntry(emote: key, mxc: value['url']));
}
});
} else if (emoteSource['short'] is Map) {
emoteSource['short'].forEach((key, value) { emoteSource['short'].forEach((key, value) {
if (key is String && value is String && value.startsWith('mxc://')) { if (key is String &&
value is String &&
value.startsWith('mxc://')) {
emotes.add(_EmoteEntry(emote: key, mxc: value)); emotes.add(_EmoteEntry(emote: key, mxc: value));
} }
}); });
} }
} }
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(L10n.of(context).emoteSettings), title: Text(L10n.of(context).emoteSettings),
@ -166,7 +241,6 @@ class _EmotesSettingsState extends State<EmotesSettings> {
size: 32.0, size: 32.0,
), ),
onTap: () async { onTap: () async {
debugPrint('blah');
if (newEmoteController.text == null || if (newEmoteController.text == null ||
newEmoteController.text.isEmpty || newEmoteController.text.isEmpty ||
newMxcController.text == null || newMxcController.text == null ||
@ -204,7 +278,19 @@ class _EmotesSettingsState extends State<EmotesSettings> {
vertical: 8.0, vertical: 8.0,
), ),
), ),
if (!readonly) if (widget.room != null)
ListTile(
title: Text(L10n.of(context).enableEmotesGlobally),
trailing: Switch(
value: isGloballyActive(client),
activeColor: Theme.of(context).primaryColor,
onChanged: (bool newValue) async {
await _setIsGloballyActive(context, newValue);
setState(() => null);
},
),
),
if (!readonly || widget.room != null)
Divider( Divider(
height: 2, height: 2,
thickness: 2, thickness: 2,

View file

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../components/adaptive_page_layout.dart';
import '../utils/app_route.dart';
import 'chat_list.dart';
import 'settings_emotes.dart';
class MultipleEmotesSettingsView extends StatelessWidget {
final Room room;
MultipleEmotesSettingsView({this.room});
@override
Widget build(BuildContext context) {
return AdaptivePageLayout(
primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(),
secondScaffold: MultipleEmotesSettings(room: room),
);
}
}
class MultipleEmotesSettings extends StatelessWidget {
final Room room;
MultipleEmotesSettings({this.room});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).emotePacks),
),
body: StreamBuilder(
stream: room.onUpdate.stream,
builder: (context, snapshot) {
final packs =
room.states.states['im.ponies.room_emotes'] ?? <String, Event>{};
if (!packs.containsKey('')) {
packs[''] = null;
}
final keys = packs.keys.toList();
keys.sort();
return ListView.separated(
separatorBuilder: (BuildContext context, int i) => Container(),
itemCount: keys.length,
itemBuilder: (BuildContext context, int i) {
final event = packs[keys[i]];
var packName = keys[i].isNotEmpty ? keys[i] : 'Default Pack';
if (event != null && event.content['pack'] is Map) {
if (event.content['pack']['displayname'] is String) {
packName = event.content['pack']['displayname'];
} else if (event.content['pack']['name'] is String) {
packName = event.content['pack']['name'];
}
}
return ListTile(
title: Text(packName),
onTap: () async {
await Navigator.of(context).push(
AppRoute.defaultRoute(
context,
EmotesSettingsView(room: room, stateKey: keys[i]),
),
);
},
);
});
},
),
);
}
}

View file

@ -49,7 +49,7 @@ packages:
name: async name: async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.5.0-nullsafety.1" version: "2.4.2"
base58check: base58check:
dependency: transitive dependency: transitive
description: description:
@ -63,7 +63,7 @@ packages:
name: boolean_selector name: boolean_selector
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0-nullsafety.1" version: "2.0.0"
bot_toast: bot_toast:
dependency: "direct main" dependency: "direct main"
description: description:
@ -91,14 +91,14 @@ packages:
name: characters name: characters
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0-nullsafety.3" version: "1.0.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
name: charcode name: charcode
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0-nullsafety.1" version: "1.1.3"
cli_util: cli_util:
dependency: transitive dependency: transitive
description: description:
@ -112,14 +112,14 @@ packages:
name: clock name: clock
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0-nullsafety.1" version: "1.0.1"
collection: collection:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.15.0-nullsafety.3" version: "1.14.13"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@ -175,13 +175,13 @@ packages:
name: fake_async name: fake_async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0-nullsafety.1" version: "1.1.0"
famedlysdk: famedlysdk:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "5019ebfeb56f0789ab4cc8d27ccda663156b5d68" ref: "84cc925b08e97098d00c54fff9c1244f91055de3"
resolved-ref: "5019ebfeb56f0789ab4cc8d27ccda663156b5d68" resolved-ref: "84cc925b08e97098d00c54fff9c1244f91055de3"
url: "https://gitlab.com/famedly/famedlysdk.git" url: "https://gitlab.com/famedly/famedlysdk.git"
source: git source: git
version: "0.0.1" version: "0.0.1"
@ -456,7 +456,7 @@ packages:
name: js name: js
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.3-nullsafety.1" version: "0.6.2"
localstorage: localstorage:
dependency: "direct main" dependency: "direct main"
description: description:
@ -484,7 +484,7 @@ packages:
name: matcher name: matcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.10-nullsafety.1" version: "0.12.8"
matrix_file_e2ee: matrix_file_e2ee:
dependency: transitive dependency: transitive
description: description:
@ -512,7 +512,7 @@ packages:
name: meta name: meta
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0-nullsafety.3" version: "1.1.8"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@ -605,7 +605,7 @@ packages:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0-nullsafety.1" version: "1.7.0"
path_provider: path_provider:
dependency: "direct main" dependency: "direct main"
description: description:
@ -647,7 +647,7 @@ packages:
name: pedantic name: pedantic
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.10.0-nullsafety.1" version: "1.9.0"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@ -689,7 +689,7 @@ packages:
name: pool name: pool
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.5.0-nullsafety.1" version: "1.4.0"
process: process:
dependency: transitive dependency: transitive
description: description:
@ -792,21 +792,21 @@ packages:
name: source_map_stack_trace name: source_map_stack_trace
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0-nullsafety.2" version: "2.0.0"
source_maps: source_maps:
dependency: transitive dependency: transitive
description: description:
name: source_maps name: source_maps
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.10.10-nullsafety.1" version: "0.10.9"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0-nullsafety.2" version: "1.7.0"
sqflite: sqflite:
dependency: "direct main" dependency: "direct main"
description: description:
@ -841,21 +841,21 @@ packages:
name: stack_trace name: stack_trace
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.10.0-nullsafety.1" version: "1.9.5"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0-nullsafety.1" version: "2.0.0"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
name: string_scanner name: string_scanner
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0-nullsafety.1" version: "1.0.5"
synchronized: synchronized:
dependency: transitive dependency: transitive
description: description:
@ -869,35 +869,35 @@ packages:
name: term_glyph name: term_glyph
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0-nullsafety.1" version: "1.1.0"
test: test:
dependency: transitive dependency: transitive
description: description:
name: test name: test
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.16.0-nullsafety.5" version: "1.15.2"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.19-nullsafety.2" version: "0.2.17"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.12-nullsafety.5" version: "0.3.10"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
name: typed_data name: typed_data
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0-nullsafety.3" version: "1.2.0"
universal_html: universal_html:
dependency: "direct main" dependency: "direct main"
description: description:
@ -981,7 +981,7 @@ packages:
name: vector_math name: vector_math
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0-nullsafety.3" version: "2.0.8"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@ -1053,5 +1053,5 @@ packages:
source: hosted source: hosted
version: "0.1.2" version: "0.1.2"
sdks: sdks:
dart: ">=2.10.0-110 <2.11.0" dart: ">=2.9.0 <3.0.0"
flutter: ">=1.20.0 <2.0.0" flutter: ">=1.20.0 <2.0.0"

View file

@ -27,7 +27,7 @@ dependencies:
famedlysdk: famedlysdk:
git: git:
url: https://gitlab.com/famedly/famedlysdk.git url: https://gitlab.com/famedly/famedlysdk.git
ref: 5019ebfeb56f0789ab4cc8d27ccda663156b5d68 ref: 84cc925b08e97098d00c54fff9c1244f91055de3
localstorage: ^3.0.1+4 localstorage: ^3.0.1+4
memoryfilepicker: ^0.1.3 memoryfilepicker: ^0.1.3