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
- Implemented ignore list
- 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) {
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(
baseOffset: startText.length,
extentOffset: startText.length,
);
});
} else {
controller.selection = TextSelection(
baseOffset: startText.length,
extentOffset: startText.length,
);
}
controller.selection = TextSelection(
baseOffset: startText.length,
extentOffset: startText.length,
);
}
}

View File

@ -529,11 +529,21 @@
"type": "text",
"placeholders": {}
},
"emotePacks": "Emote packs for room",
"@emotePacks": {
"type": "text",
"placeholders": {}
},
"emptyChat": "Empty chat",
"@emptyChat": {
"type": "text",
"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": {
"type": "text",
@ -1708,4 +1718,4 @@
"type": "text",
"placeholders": {}
}
}
}

View File

@ -19,6 +19,7 @@ import 'package:matrix_link_text/link_text.dart';
import 'package:memoryfilepicker/memoryfilepicker.dart';
import './settings_emotes.dart';
import './settings_multiple_emotes.dart';
import '../utils/url_launcher.dart';
class ChatDetails extends StatefulWidget {
@ -285,13 +286,31 @@ class _ChatDetailsState extends State<ChatDetails> {
child: Icon(Icons.insert_emoticon),
),
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,
EmotesSettingsView(room: widget.room),
),
),
AppRoute.defaultRoute(
context,
MultipleEmotesSettingsView(
room: widget.room),
),
);
} else {
await Navigator.of(context).push(
AppRoute.defaultRoute(
context,
EmotesSettingsView(room: widget.room),
),
);
}
},
),
PopupMenuButton(
child: ListTile(

View File

@ -14,23 +14,25 @@ import 'chat_list.dart';
class EmotesSettingsView extends StatelessWidget {
final Room room;
final String stateKey;
EmotesSettingsView({this.room});
EmotesSettingsView({this.room, this.stateKey});
@override
Widget build(BuildContext context) {
return AdaptivePageLayout(
primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(),
secondScaffold: EmotesSettings(room: room),
secondScaffold: EmotesSettings(room: room, stateKey: stateKey),
);
}
}
class EmotesSettings extends StatefulWidget {
final Room room;
final String stateKey;
EmotesSettings({this.room});
EmotesSettings({this.room, this.stateKey});
@override
_EmotesSettingsState createState() => _EmotesSettingsState();
@ -59,21 +61,42 @@ class _EmotesSettingsState extends State<EmotesSettings> {
// be sure to preserve any data not in "short"
Map<String, dynamic> content;
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>{};
} else {
content = client.accountData['im.ponies.user_emotes']?.content ??
<String, dynamic>{};
}
debugPrint(content.toString());
content['short'] = <String, String>{};
for (final emote in emotes) {
content['short'][emote.emote] = emote.mxc;
if (!(content['emoticons'] is Map)) {
content['emoticons'] = <String, dynamic>{};
}
// 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());
if (widget.room != null) {
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 {
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
? false
: !(widget.room.canSendEvent('im.ponies.room_emotes'));
@ -93,16 +153,31 @@ class _EmotesSettingsState extends State<EmotesSettings> {
emotes = <_EmoteEntry>[];
Map<String, dynamic> emoteSource;
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 {
emoteSource = client.accountData['im.ponies.user_emotes']?.content;
}
if (emoteSource != null && emoteSource['short'] is Map) {
emoteSource['short'].forEach((key, value) {
if (key is String && value is String && value.startsWith('mxc://')) {
emotes.add(_EmoteEntry(emote: key, mxc: value));
}
});
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) {
if (key is String &&
value is String &&
value.startsWith('mxc://')) {
emotes.add(_EmoteEntry(emote: key, mxc: value));
}
});
}
}
}
return Scaffold(
@ -166,7 +241,6 @@ class _EmotesSettingsState extends State<EmotesSettings> {
size: 32.0,
),
onTap: () async {
debugPrint('blah');
if (newEmoteController.text == null ||
newEmoteController.text.isEmpty ||
newMxcController.text == null ||
@ -204,7 +278,19 @@ class _EmotesSettingsState extends State<EmotesSettings> {
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(
height: 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
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0-nullsafety.1"
version: "2.4.2"
base58check:
dependency: transitive
description:
@ -63,7 +63,7 @@ packages:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
version: "2.0.0"
bot_toast:
dependency: "direct main"
description:
@ -91,14 +91,14 @@ packages:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.3"
version: "1.0.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.1.3"
cli_util:
dependency: transitive
description:
@ -112,14 +112,14 @@ packages:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
version: "1.0.1"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0-nullsafety.3"
version: "1.14.13"
convert:
dependency: transitive
description:
@ -175,13 +175,13 @@ packages:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.1.0"
famedlysdk:
dependency: "direct main"
description:
path: "."
ref: "5019ebfeb56f0789ab4cc8d27ccda663156b5d68"
resolved-ref: "5019ebfeb56f0789ab4cc8d27ccda663156b5d68"
ref: "84cc925b08e97098d00c54fff9c1244f91055de3"
resolved-ref: "84cc925b08e97098d00c54fff9c1244f91055de3"
url: "https://gitlab.com/famedly/famedlysdk.git"
source: git
version: "0.0.1"
@ -456,7 +456,7 @@ packages:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3-nullsafety.1"
version: "0.6.2"
localstorage:
dependency: "direct main"
description:
@ -484,7 +484,7 @@ packages:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10-nullsafety.1"
version: "0.12.8"
matrix_file_e2ee:
dependency: transitive
description:
@ -512,7 +512,7 @@ packages:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
version: "1.1.8"
mime:
dependency: transitive
description:
@ -605,7 +605,7 @@ packages:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.1"
version: "1.7.0"
path_provider:
dependency: "direct main"
description:
@ -647,7 +647,7 @@ packages:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0-nullsafety.1"
version: "1.9.0"
petitparser:
dependency: transitive
description:
@ -689,7 +689,7 @@ packages:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.0-nullsafety.1"
version: "1.4.0"
process:
dependency: transitive
description:
@ -792,21 +792,21 @@ packages:
name: source_map_stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.2"
version: "2.0.0"
source_maps:
dependency: transitive
description:
name: source_maps
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.10-nullsafety.1"
version: "0.10.9"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.2"
version: "1.7.0"
sqflite:
dependency: "direct main"
description:
@ -841,21 +841,21 @@ packages:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0-nullsafety.1"
version: "1.9.5"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
version: "2.0.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
version: "1.0.5"
synchronized:
dependency: transitive
description:
@ -869,35 +869,35 @@ packages:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.1.0"
test:
dependency: transitive
description:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.16.0-nullsafety.5"
version: "1.15.2"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19-nullsafety.2"
version: "0.2.17"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.12-nullsafety.5"
version: "0.3.10"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
version: "1.2.0"
universal_html:
dependency: "direct main"
description:
@ -981,7 +981,7 @@ packages:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.3"
version: "2.0.8"
vm_service:
dependency: transitive
description:
@ -1053,5 +1053,5 @@ packages:
source: hosted
version: "0.1.2"
sdks:
dart: ">=2.10.0-110 <2.11.0"
dart: ">=2.9.0 <3.0.0"
flutter: ">=1.20.0 <2.0.0"

View File

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