Merge branch 'main' into 481-use-forked-matrix-sdk
This commit is contained in:
commit
6f37cd014c
7 changed files with 214 additions and 133 deletions
|
|
@ -4111,5 +4111,7 @@
|
|||
"deleteSubscriptionWarningBody": "Deleting your account will not automatically cancel your subscription.",
|
||||
"manageSubscription": "Manage Subscription",
|
||||
"createSpace": "Create space",
|
||||
"createChat": "Create chat"
|
||||
"createChat": "Create chat",
|
||||
"error520Title": "Please try again.",
|
||||
"error520Desc": "Sorry, we could not understand your message..."
|
||||
}
|
||||
|
|
@ -314,8 +314,9 @@ class Message extends StatelessWidget {
|
|||
padding: const EdgeInsets.only(left: 8),
|
||||
child: GestureDetector(
|
||||
// #Pangea
|
||||
onTap: () =>
|
||||
toolbarController?.showToolbar(context),
|
||||
onTap: () => toolbarController?.showToolbar(
|
||||
context,
|
||||
),
|
||||
onDoubleTap: () =>
|
||||
toolbarController?.showToolbar(context),
|
||||
// Pangea#
|
||||
|
|
@ -585,7 +586,9 @@ class Message extends StatelessWidget {
|
|||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
if (pangeaMessageEvent?.showMessageButtons ?? false)
|
||||
MessageButtons(toolbarController: toolbarController),
|
||||
MessageButtons(
|
||||
toolbarController: toolbarController,
|
||||
),
|
||||
MessageReactions(event, timeline),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -476,6 +476,8 @@ class ChatListController extends State<ChatList>
|
|||
StreamSubscription? classStream;
|
||||
StreamSubscription? _invitedSpaceSubscription;
|
||||
StreamSubscription? _subscriptionStatusStream;
|
||||
StreamSubscription? _spaceChildSubscription;
|
||||
final Set<String> hasUpdates = {};
|
||||
//Pangea#
|
||||
|
||||
@override
|
||||
|
|
@ -567,6 +569,16 @@ class ChatListController extends State<ChatList>
|
|||
showSubscribedSnackbar(context);
|
||||
}
|
||||
});
|
||||
|
||||
// listen for space child updates for any space that is not the active space
|
||||
// so that when the user navigates to the space that was updated, it will
|
||||
// reload any rooms that have been added / removed
|
||||
final client = pangeaController.matrixState.client;
|
||||
_spaceChildSubscription ??= client.onRoomState.stream.where((u) {
|
||||
return u.state.type == EventTypes.SpaceChild && u.roomId != activeSpaceId;
|
||||
}).listen((update) {
|
||||
hasUpdates.add(update.roomId);
|
||||
});
|
||||
//Pangea#
|
||||
|
||||
super.initState();
|
||||
|
|
@ -581,6 +593,7 @@ class ChatListController extends State<ChatList>
|
|||
classStream?.cancel();
|
||||
_invitedSpaceSubscription?.cancel();
|
||||
_subscriptionStatusStream?.cancel();
|
||||
_spaceChildSubscription?.cancel();
|
||||
//Pangea#
|
||||
scrollController.removeListener(_onScroll);
|
||||
super.dispose();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart';
|
|||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/sync_update_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
|
|
@ -46,8 +45,8 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
Object? error;
|
||||
bool loading = false;
|
||||
// #Pangea
|
||||
StreamSubscription<SyncUpdate>? _roomSubscription;
|
||||
bool refreshing = false;
|
||||
StreamSubscription? _roomSubscription;
|
||||
|
||||
final String _chatCountsKey = 'chatCounts';
|
||||
Map<String, int> get chatCounts => Map.from(
|
||||
|
|
@ -58,9 +57,33 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
loadHierarchy();
|
||||
// #Pangea
|
||||
// loadHierarchy();
|
||||
|
||||
// If, on launch, this room has had updates to its children,
|
||||
// ensure the hierarchy is properly reloaded
|
||||
final bool hasUpdate = widget.controller.hasUpdates.contains(
|
||||
widget.controller.activeSpaceId,
|
||||
);
|
||||
|
||||
loadHierarchy(hasUpdate: hasUpdate).then(
|
||||
// remove this space ID from the set of space IDs with updates
|
||||
(_) => widget.controller.hasUpdates.remove(
|
||||
widget.controller.activeSpaceId,
|
||||
),
|
||||
);
|
||||
|
||||
loadChatCounts();
|
||||
|
||||
// Listen for changes to the activeSpace's hierarchy,
|
||||
// and reload the hierarchy when they come through
|
||||
final client = Matrix.of(context).client;
|
||||
_roomSubscription ??= client.onRoomState.stream.where((u) {
|
||||
return u.state.type == EventTypes.SpaceChild &&
|
||||
u.roomId == widget.controller.activeSpaceId;
|
||||
}).listen((update) {
|
||||
loadHierarchy(hasUpdate: true);
|
||||
});
|
||||
// Pangea#
|
||||
super.initState();
|
||||
}
|
||||
|
|
@ -76,11 +99,11 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
void _refresh() {
|
||||
// #Pangea
|
||||
// _lastResponse.remove(widget.controller.activseSpaceId);
|
||||
if (mounted) {
|
||||
// Pangea#
|
||||
loadHierarchy();
|
||||
// #Pangea
|
||||
}
|
||||
// loadHierarchy();
|
||||
if (mounted) setState(() => refreshing = true);
|
||||
loadHierarchy(hasUpdate: true).whenComplete(() {
|
||||
if (mounted) setState(() => refreshing = false);
|
||||
});
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
|
|
@ -129,8 +152,10 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
/// spaceId, it will try to load the next batch and add the new rooms to the
|
||||
/// already loaded ones. Displays a loading indicator while loading, and an error
|
||||
/// message if an error occurs.
|
||||
/// If hasUpdate is true, it will force the hierarchy to be reloaded.
|
||||
Future<void> loadHierarchy({
|
||||
String? spaceId,
|
||||
bool hasUpdate = false,
|
||||
}) async {
|
||||
if ((widget.controller.activeSpaceId == null && spaceId == null) ||
|
||||
loading) {
|
||||
|
|
@ -142,7 +167,7 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
setState(() {});
|
||||
|
||||
try {
|
||||
await _loadHierarchy(spaceId: spaceId);
|
||||
await _loadHierarchy(spaceId: spaceId, hasUpdate: hasUpdate);
|
||||
} catch (e, s) {
|
||||
if (mounted) {
|
||||
setState(() => error = e);
|
||||
|
|
@ -159,6 +184,7 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
/// the active space id (or specified spaceId).
|
||||
Future<void> _loadHierarchy({
|
||||
String? spaceId,
|
||||
bool hasUpdate = false,
|
||||
}) async {
|
||||
final client = Matrix.of(context).client;
|
||||
final activeSpaceId = (widget.controller.activeSpaceId ?? spaceId)!;
|
||||
|
|
@ -177,7 +203,7 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
await activeSpace.postLoad();
|
||||
|
||||
// The current number of rooms loaded for this space that are visible in the UI
|
||||
final int prevLength = _lastResponse[activeSpaceId] != null
|
||||
final int prevLength = _lastResponse[activeSpaceId] != null && !hasUpdate
|
||||
? filterHierarchyResponse(
|
||||
activeSpace,
|
||||
_lastResponse[activeSpaceId]!.rooms,
|
||||
|
|
@ -187,6 +213,9 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
// Failsafe to prevent too many calls to the server in a row
|
||||
int callsToServer = 0;
|
||||
|
||||
GetSpaceHierarchyResponse? currentHierarchy =
|
||||
hasUpdate ? null : _lastResponse[activeSpaceId];
|
||||
|
||||
// Makes repeated calls to the server until 10 new visible rooms have
|
||||
// been loaded, or there are no rooms left to load. Using a loop here,
|
||||
// rather than one single call to the endpoint, because some spaces have
|
||||
|
|
@ -195,16 +224,15 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
// coming through from those calls are analytics rooms).
|
||||
while (callsToServer < 5) {
|
||||
// if this space has been loaded and there are no more rooms to load, break
|
||||
if (_lastResponse[activeSpaceId] != null &&
|
||||
_lastResponse[activeSpaceId]!.nextBatch == null) {
|
||||
if (currentHierarchy != null && currentHierarchy.nextBatch == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
// if this space has been loaded and 10 new rooms have been loaded, break
|
||||
if (_lastResponse[activeSpaceId] != null) {
|
||||
if (currentHierarchy != null) {
|
||||
final int currentLength = filterHierarchyResponse(
|
||||
activeSpace,
|
||||
_lastResponse[activeSpaceId]!.rooms,
|
||||
currentHierarchy.rooms,
|
||||
).length;
|
||||
|
||||
if (currentLength - prevLength >= 10) {
|
||||
|
|
@ -216,22 +244,26 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
final response = await client.getSpaceHierarchy(
|
||||
activeSpaceId,
|
||||
maxDepth: 1,
|
||||
from: _lastResponse[activeSpaceId]?.nextBatch,
|
||||
from: currentHierarchy?.nextBatch,
|
||||
limit: 100,
|
||||
);
|
||||
callsToServer++;
|
||||
|
||||
// if rooms have earlier been loaded for this space, add those
|
||||
// previously loaded rooms to the front of the response list
|
||||
if (_lastResponse[activeSpaceId] != null) {
|
||||
if (currentHierarchy != null) {
|
||||
response.rooms.insertAll(
|
||||
0,
|
||||
_lastResponse[activeSpaceId]?.rooms ?? [],
|
||||
currentHierarchy.rooms,
|
||||
);
|
||||
}
|
||||
|
||||
// finally, set the response to the last response for this space
|
||||
_lastResponse[activeSpaceId] = response;
|
||||
currentHierarchy = response;
|
||||
}
|
||||
|
||||
if (currentHierarchy != null) {
|
||||
_lastResponse[activeSpaceId] = currentHierarchy;
|
||||
}
|
||||
|
||||
// After making those calls to the server, set the chat count for
|
||||
|
|
@ -560,34 +592,6 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
}
|
||||
}
|
||||
|
||||
void refreshOnUpdate(SyncUpdate event) {
|
||||
/* refresh on leave, invite, and space child update
|
||||
not join events, because there's already a listener on
|
||||
onTapSpaceChild, and they interfere with each other */
|
||||
if (widget.controller.activeSpaceId == null || !mounted || refreshing) {
|
||||
return;
|
||||
}
|
||||
setState(() => refreshing = true);
|
||||
final client = Matrix.of(context).client;
|
||||
if (mounted &&
|
||||
event.isMembershipUpdateByType(
|
||||
Membership.leave,
|
||||
client.userID!,
|
||||
) ||
|
||||
event.isMembershipUpdateByType(
|
||||
Membership.invite,
|
||||
client.userID!,
|
||||
) ||
|
||||
event.isSpaceChildUpdate(
|
||||
widget.controller.activeSpaceId!,
|
||||
)) {
|
||||
debugPrint("refresh on update");
|
||||
loadHierarchy().whenComplete(() {
|
||||
if (mounted) setState(() => refreshing = false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool includeSpaceChild(
|
||||
Room space,
|
||||
SpaceRoomsChunk hierarchyMember,
|
||||
|
|
@ -769,12 +773,6 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
);
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
_roomSubscription ??= client.onSync.stream
|
||||
.where((event) => event.hasRoomUpdate)
|
||||
.listen(refreshOnUpdate);
|
||||
// Pangea#
|
||||
|
||||
final parentSpace = allSpaces.firstWhereOrNull(
|
||||
(space) =>
|
||||
space.spaceChildren.any((child) => child.roomId == activeSpaceId),
|
||||
|
|
|
|||
|
|
@ -122,6 +122,10 @@ class ErrorCopy {
|
|||
title = l10n.error502504Title;
|
||||
body = l10n.error502504Desc;
|
||||
break;
|
||||
case 520:
|
||||
title = l10n.error520Title;
|
||||
body = l10n.error520Desc;
|
||||
break;
|
||||
case 404:
|
||||
title = l10n.error404Title;
|
||||
body = l10n.error404Desc;
|
||||
|
|
|
|||
|
|
@ -58,7 +58,10 @@ class ToolbarDisplayController {
|
|||
);
|
||||
}
|
||||
|
||||
void showToolbar(BuildContext context, {MessageMode? mode}) {
|
||||
void showToolbar(
|
||||
BuildContext context, {
|
||||
MessageMode? mode,
|
||||
}) {
|
||||
bool toolbarUp = true;
|
||||
if (highlighted) return;
|
||||
if (controller.selectMode) {
|
||||
|
|
@ -78,8 +81,51 @@ class ToolbarDisplayController {
|
|||
final Size transformTargetSize = (targetRenderBox as RenderBox).size;
|
||||
messageWidth = transformTargetSize.width;
|
||||
final Offset targetOffset = (targetRenderBox).localToGlobal(Offset.zero);
|
||||
final double screenHeight = MediaQuery.of(context).size.height;
|
||||
toolbarUp = targetOffset.dy >= screenHeight / 2;
|
||||
|
||||
// If there is enough space above, procede as normal
|
||||
// Else if there is enough space below, show toolbar underneath
|
||||
if (targetOffset.dy < 320) {
|
||||
final spaceBeneath = MediaQuery.of(context).size.height -
|
||||
(targetOffset.dy + transformTargetSize.height);
|
||||
if (spaceBeneath >= 320) {
|
||||
toolbarUp = false;
|
||||
}
|
||||
|
||||
// See if it's possible to scroll up to make space
|
||||
else if (controller.scrollController.offset - targetOffset.dy + 320 >=
|
||||
controller.scrollController.position.minScrollExtent &&
|
||||
controller.scrollController.offset - targetOffset.dy + 320 <=
|
||||
controller.scrollController.position.maxScrollExtent) {
|
||||
controller.scrollController.animateTo(
|
||||
controller.scrollController.offset - targetOffset.dy + 320,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
);
|
||||
}
|
||||
|
||||
// See if it's possible to scroll down to make space
|
||||
else if (controller.scrollController.offset + spaceBeneath - 320 >=
|
||||
controller.scrollController.position.minScrollExtent &&
|
||||
controller.scrollController.offset + spaceBeneath - 320 <=
|
||||
controller.scrollController.position.maxScrollExtent) {
|
||||
controller.scrollController.animateTo(
|
||||
controller.scrollController.offset + spaceBeneath - 320,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
);
|
||||
toolbarUp = false;
|
||||
}
|
||||
|
||||
// If message is too big and can't scroll either way
|
||||
// Scroll up as much as possible, and show toolbar above
|
||||
else {
|
||||
controller.scrollController.animateTo(
|
||||
controller.scrollController.position.minScrollExtent,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Widget overlayMessage = OverlayMessage(
|
||||
|
|
@ -106,7 +152,13 @@ class ToolbarDisplayController {
|
|||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
toolbarUp ? toolbar! : overlayMessage,
|
||||
toolbarUp
|
||||
// Column is limited to screen height
|
||||
// If message portion is too tall, decrease toolbar height
|
||||
// as necessary to prevent toolbar from acting strange
|
||||
// Problems may still occur if toolbar height is decreased too much
|
||||
? toolbar!
|
||||
: overlayMessage,
|
||||
const SizedBox(height: 6),
|
||||
toolbarUp ? overlayMessage : toolbar!,
|
||||
],
|
||||
|
|
@ -367,83 +419,85 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
return Flexible(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(25),
|
||||
),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(25),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 300,
|
||||
minWidth: 300,
|
||||
maxHeight: 300,
|
||||
),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 300,
|
||||
minWidth: 300,
|
||||
maxHeight: 300,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: toolbarContent ?? const SizedBox(),
|
||||
),
|
||||
SizedBox(height: toolbarContent == null ? 0 : 20),
|
||||
],
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: toolbarContent ?? const SizedBox(),
|
||||
),
|
||||
SizedBox(height: toolbarContent == null ? 0 : 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: MessageMode.values.map((mode) {
|
||||
if ([
|
||||
MessageMode.definition,
|
||||
MessageMode.textToSpeech,
|
||||
MessageMode.translation,
|
||||
].contains(mode) &&
|
||||
widget.pangeaMessageEvent.isAudioMessage) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (mode == MessageMode.speechToText &&
|
||||
!widget.pangeaMessageEvent.isAudioMessage) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Tooltip(
|
||||
message: mode.tooltip(context),
|
||||
child: IconButton(
|
||||
icon: Icon(mode.icon),
|
||||
color: mode.iconColor(
|
||||
widget.pangeaMessageEvent,
|
||||
currentMode,
|
||||
context,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: MessageMode.values.map((mode) {
|
||||
if ([
|
||||
MessageMode.definition,
|
||||
MessageMode.textToSpeech,
|
||||
MessageMode.translation,
|
||||
].contains(mode) &&
|
||||
widget.pangeaMessageEvent.isAudioMessage) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (mode == MessageMode.speechToText &&
|
||||
!widget.pangeaMessageEvent.isAudioMessage) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Tooltip(
|
||||
message: mode.tooltip(context),
|
||||
child: IconButton(
|
||||
icon: Icon(mode.icon),
|
||||
color: mode.iconColor(
|
||||
widget.pangeaMessageEvent,
|
||||
currentMode,
|
||||
context,
|
||||
),
|
||||
onPressed: () => updateMode(mode),
|
||||
),
|
||||
);
|
||||
}).toList() +
|
||||
[
|
||||
Tooltip(
|
||||
message: L10n.of(context)!.more,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.add_reaction_outlined),
|
||||
onPressed: showMore,
|
||||
),
|
||||
onPressed: () => updateMode(mode),
|
||||
),
|
||||
);
|
||||
}).toList() +
|
||||
[
|
||||
Tooltip(
|
||||
message: L10n.of(context)!.more,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.add_reaction_outlined),
|
||||
onPressed: showMore,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator
|
|||
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class MessageTranslationCard extends StatefulWidget {
|
||||
final PangeaMessageEvent messageEvent;
|
||||
|
|
@ -140,9 +141,15 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
return const CardErrorWidget();
|
||||
}
|
||||
|
||||
final bool showWarning = l2Code != null &&
|
||||
!widget.immersionMode &&
|
||||
widget.messageEvent.originalSent?.langCode != l2Code &&
|
||||
// Show warning if message's language code is user's L1
|
||||
// or if translated text is same as original text
|
||||
// Warning does not show if was previously closed
|
||||
final bool showWarning = widget.messageEvent.originalSent != null &&
|
||||
((!widget.immersionMode &&
|
||||
widget.messageEvent.originalSent!.langCode.equals(l1Code)) ||
|
||||
(selectionTranslation == null ||
|
||||
widget.messageEvent.originalSent!.text
|
||||
.equals(selectionTranslation))) &&
|
||||
!MatrixState.pangeaController.instructions.wereInstructionsTurnedOff(
|
||||
InlineInstructions.l1Translation.toString(),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue