Merge branch 'main' into move-chat-buttons
This commit is contained in:
commit
db168c805f
10 changed files with 242 additions and 191 deletions
|
|
@ -476,10 +476,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
if (kIsWeb && !Matrix.of(context).webHasFocus) return;
|
||||
// #Pangea
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: PangeaWarningError("Web focus error: $err"),
|
||||
s: s,
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Pangea#
|
||||
|
|
|
|||
|
|
@ -289,17 +289,20 @@ class MessageContent extends StatelessWidget {
|
|||
// #Pangea
|
||||
// return Linkify(
|
||||
final messageTextStyle = TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: textColor,
|
||||
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
||||
decoration: event.redacted ? TextDecoration.lineThrough : null,
|
||||
height: 1.3,
|
||||
);
|
||||
if (immersionMode && pangeaMessageEvent != null) {
|
||||
return PangeaRichText(
|
||||
style: messageTextStyle,
|
||||
pangeaMessageEvent: pangeaMessageEvent!,
|
||||
immersionMode: immersionMode,
|
||||
toolbarController: toolbarController,
|
||||
return Flexible(
|
||||
child: PangeaRichText(
|
||||
style: messageTextStyle,
|
||||
pangeaMessageEvent: pangeaMessageEvent!,
|
||||
immersionMode: immersionMode,
|
||||
toolbarController: toolbarController,
|
||||
),
|
||||
);
|
||||
} else if (pangeaMessageEvent != null) {
|
||||
toolbarController?.toolbar?.textSelection.setMessageText(
|
||||
|
|
|
|||
|
|
@ -504,6 +504,9 @@ class InputBar extends StatelessWidget {
|
|||
onSubmitted!(text);
|
||||
},
|
||||
// #Pangea
|
||||
style: controller?.isMaxLength ?? false
|
||||
? const TextStyle(color: Colors.red)
|
||||
: null,
|
||||
onTap: () {
|
||||
controller!.onInputTap(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:fluffychat/pangea/constants/age_limits.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/base_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
|
|
@ -36,63 +35,73 @@ class PermissionsController extends BaseController {
|
|||
return dob?.isAtLeastYearsOld(AgeLimits.toAccessFeatures) ?? false;
|
||||
}
|
||||
|
||||
/// A user can private chat if
|
||||
/// 1) they are 18 and outside a class context or
|
||||
/// 2) they are in a class context and the class rules permit it
|
||||
/// If no class is passed, uses classController.activeClass
|
||||
/// A user can private chat if they are 18+
|
||||
bool canUserPrivateChat({String? roomID}) {
|
||||
final Room? classContext =
|
||||
firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules);
|
||||
return classContext?.pangeaRoomRules == null
|
||||
? isUser18()
|
||||
: classContext!.pangeaRoomRules!.oneToOneChatClass ||
|
||||
classContext.isRoomAdmin;
|
||||
return isUser18();
|
||||
// Rules can't be edited; default to true
|
||||
// final Room? classContext =
|
||||
// firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules);
|
||||
// return classContext?.pangeaRoomRules == null
|
||||
// ? isUser18()
|
||||
// : classContext!.pangeaRoomRules!.oneToOneChatClass ||
|
||||
// classContext.isRoomAdmin;
|
||||
}
|
||||
|
||||
bool canUserGroupChat({String? roomID}) {
|
||||
final Room? classContext =
|
||||
firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules);
|
||||
return isUser18();
|
||||
// Rules can't be edited; default to true
|
||||
// final Room? classContext =
|
||||
// firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules);
|
||||
|
||||
return classContext?.pangeaRoomRules == null
|
||||
? isUser18()
|
||||
: classContext!.pangeaRoomRules!.isCreateRooms ||
|
||||
classContext.isRoomAdmin;
|
||||
// return classContext?.pangeaRoomRules == null
|
||||
// ? isUser18()
|
||||
// : classContext!.pangeaRoomRules!.isCreateRooms ||
|
||||
// classContext.isRoomAdmin;
|
||||
}
|
||||
|
||||
bool showChatInputAddButton(String roomId) {
|
||||
final PangeaRoomRules? perms = _getRoomRules(roomId);
|
||||
if (perms == null) return isUser18();
|
||||
return perms.isShareFiles ||
|
||||
perms.isShareLocation ||
|
||||
perms.isSharePhoto ||
|
||||
perms.isShareVideo;
|
||||
// Rules can't be edited; default to true
|
||||
// final PangeaRoomRules? perms = _getRoomRules(roomId);
|
||||
// if (perms == null) return isUser18();
|
||||
// return perms.isShareFiles ||
|
||||
// perms.isShareLocation ||
|
||||
// perms.isSharePhoto ||
|
||||
// perms.isShareVideo;
|
||||
return isUser18();
|
||||
}
|
||||
|
||||
/// works for both roomID of chat and class
|
||||
bool canShareVideo(String? roomID) =>
|
||||
_getRoomRules(roomID)?.isShareVideo ?? isUser18();
|
||||
bool canShareVideo(String? roomID) => isUser18();
|
||||
// Rules can't be edited; default to true
|
||||
// _getRoomRules(roomID)?.isShareVideo ?? isUser18();
|
||||
|
||||
/// works for both roomID of chat and class
|
||||
bool canSharePhoto(String? roomID) =>
|
||||
_getRoomRules(roomID)?.isSharePhoto ?? isUser18();
|
||||
bool canSharePhoto(String? roomID) => isUser18();
|
||||
// Rules can't be edited; default to true
|
||||
// _getRoomRules(roomID)?.isSharePhoto ?? isUser18();
|
||||
|
||||
/// works for both roomID of chat and class
|
||||
bool canShareFile(String? roomID) =>
|
||||
_getRoomRules(roomID)?.isShareFiles ?? isUser18();
|
||||
bool canShareFile(String? roomID) => isUser18();
|
||||
// Rules can't be edited; default to true
|
||||
// _getRoomRules(roomID)?.isShareFiles ?? isUser18();
|
||||
|
||||
/// works for both roomID of chat and class
|
||||
bool canShareLocation(String? roomID) =>
|
||||
_getRoomRules(roomID)?.isShareLocation ?? isUser18();
|
||||
bool canShareLocation(String? roomID) => isUser18();
|
||||
// Rules can't be edited; default to true
|
||||
// _getRoomRules(roomID)?.isShareLocation ?? isUser18();
|
||||
|
||||
int? classLanguageToolPermission(Room room, ToolSetting setting) =>
|
||||
room.firstRules?.getToolSettings(setting);
|
||||
int? classLanguageToolPermission(Room room, ToolSetting setting) => 1;
|
||||
// Rules can't be edited; default to student choice
|
||||
// room.firstRules?.getToolSettings(setting);
|
||||
|
||||
//what happens if a room isn't in a class?
|
||||
// what happens if a room isn't in a class?
|
||||
bool isToolDisabledByClass(ToolSetting setting, Room? room) {
|
||||
if (room?.isSpaceAdmin ?? false) return false;
|
||||
final int? classPermission =
|
||||
room != null ? classLanguageToolPermission(room, setting) : 1;
|
||||
return classPermission == 0;
|
||||
return false;
|
||||
// Rules can't be edited; default to false
|
||||
// if (room?.isSpaceAdmin ?? false) return false;
|
||||
// final int? classPermission =
|
||||
// room != null ? classLanguageToolPermission(room, setting) : 1;
|
||||
// return classPermission == 0;
|
||||
}
|
||||
|
||||
bool userToolSetting(ToolSetting setting) {
|
||||
|
|
@ -117,18 +126,22 @@ class PermissionsController extends BaseController {
|
|||
}
|
||||
|
||||
bool isToolEnabled(ToolSetting setting, Room? room) {
|
||||
if (room?.isSpaceAdmin ?? false) {
|
||||
return userToolSetting(setting);
|
||||
}
|
||||
final int? classPermission =
|
||||
room != null ? classLanguageToolPermission(room, setting) : 1;
|
||||
if (classPermission == 0) return false;
|
||||
if (classPermission == 2) return true;
|
||||
// Rules can't be edited; default to true
|
||||
return userToolSetting(setting);
|
||||
// if (room?.isSpaceAdmin ?? false) {
|
||||
// return userToolSetting(setting);
|
||||
// }
|
||||
// final int? classPermission =
|
||||
// room != null ? classLanguageToolPermission(room, setting) : 1;
|
||||
// if (classPermission == 0) return false;
|
||||
// if (classPermission == 2) return true;
|
||||
// return userToolSetting(setting);
|
||||
}
|
||||
|
||||
bool isWritingAssistanceEnabled(Room? room) {
|
||||
return isToolEnabled(ToolSetting.interactiveTranslator, room) &&
|
||||
isToolEnabled(ToolSetting.interactiveGrammar, room);
|
||||
// Rules can't be edited; default to true
|
||||
return true;
|
||||
// return isToolEnabled(ToolSetting.interactiveTranslator, room) &&
|
||||
// isToolEnabled(ToolSetting.interactiveGrammar, room);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ extension AnalyticsRoomExtension on Room {
|
|||
return Future.value();
|
||||
}
|
||||
|
||||
// Checks that user has permission to add child to space
|
||||
if (!canSendEvent(EventTypes.SpaceChild)) return;
|
||||
if (spaceChildren.any((sc) => sc.roomId == analyticsRoom.id)) return;
|
||||
|
||||
|
|
@ -103,17 +104,19 @@ extension AnalyticsRoomExtension on Room {
|
|||
.where((teacher) => !participants.contains(teacher))
|
||||
.toList();
|
||||
|
||||
Future.wait(
|
||||
uninvitedTeachers.map(
|
||||
(teacher) => analyticsRoom.invite(teacher.id).catchError((err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}",
|
||||
s: s,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
if (analyticsRoom.canSendEvent(EventTypes.RoomMember)) {
|
||||
Future.wait(
|
||||
uninvitedTeachers.map(
|
||||
(teacher) => analyticsRoom.invite(teacher.id).catchError((err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}",
|
||||
s: s,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Invite all the user's teachers to 1 analytics room.
|
||||
|
|
|
|||
|
|
@ -13,36 +13,40 @@ class AnalyticsViewButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<BarChartViewSelection>(
|
||||
tooltip: L10n.of(context)!.changeAnalyticsView,
|
||||
initialValue: value,
|
||||
onSelected: (BarChartViewSelection? view) {
|
||||
if (view == null) {
|
||||
debugPrint("when is view null?");
|
||||
return;
|
||||
}
|
||||
onChange(view);
|
||||
},
|
||||
itemBuilder: (BuildContext context) => BarChartViewSelection.values
|
||||
.map<PopupMenuEntry<BarChartViewSelection>>(
|
||||
(BarChartViewSelection view) {
|
||||
return PopupMenuItem<BarChartViewSelection>(
|
||||
value: view,
|
||||
child: Text(view.string(context)),
|
||||
);
|
||||
}).toList(),
|
||||
child: TextButton.icon(
|
||||
label: Text(
|
||||
value.string(context),
|
||||
style: TextStyle(
|
||||
return Flexible(
|
||||
child: PopupMenuButton<BarChartViewSelection>(
|
||||
tooltip: L10n.of(context)!.changeAnalyticsView,
|
||||
initialValue: value,
|
||||
onSelected: (BarChartViewSelection? view) {
|
||||
if (view == null) {
|
||||
debugPrint("when is view null?");
|
||||
return;
|
||||
}
|
||||
onChange(view);
|
||||
},
|
||||
itemBuilder: (BuildContext context) => BarChartViewSelection.values
|
||||
.map<PopupMenuEntry<BarChartViewSelection>>(
|
||||
(BarChartViewSelection view) {
|
||||
return PopupMenuItem<BarChartViewSelection>(
|
||||
value: view,
|
||||
child: Text(
|
||||
view.string(context),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
child: TextButton.icon(
|
||||
label: Text(
|
||||
value.string(context),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
value.icon,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onPressed: null,
|
||||
),
|
||||
icon: Icon(
|
||||
value.icon,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onPressed: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,35 +14,37 @@ class TimeSpanMenuButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<TimeSpan>(
|
||||
tooltip: L10n.of(context)!.changeDateRange,
|
||||
initialValue: value,
|
||||
onSelected: (TimeSpan? timeSpan) {
|
||||
if (timeSpan == null) {
|
||||
debugPrint("when is timeSpan null?");
|
||||
return;
|
||||
}
|
||||
onChange(timeSpan);
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
TimeSpan.values.map<PopupMenuEntry<TimeSpan>>((TimeSpan timeSpan) {
|
||||
return PopupMenuItem<TimeSpan>(
|
||||
value: timeSpan,
|
||||
child: Text(timeSpan.string(context)),
|
||||
);
|
||||
}).toList(),
|
||||
child: TextButton.icon(
|
||||
label: Text(
|
||||
value.string(context),
|
||||
style: TextStyle(
|
||||
return Flexible(
|
||||
child: PopupMenuButton<TimeSpan>(
|
||||
tooltip: L10n.of(context)!.changeDateRange,
|
||||
initialValue: value,
|
||||
onSelected: (TimeSpan? timeSpan) {
|
||||
if (timeSpan == null) {
|
||||
debugPrint("when is timeSpan null?");
|
||||
return;
|
||||
}
|
||||
onChange(timeSpan);
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
TimeSpan.values.map<PopupMenuEntry<TimeSpan>>((TimeSpan timeSpan) {
|
||||
return PopupMenuItem<TimeSpan>(
|
||||
value: timeSpan,
|
||||
child: Text(timeSpan.string(context)),
|
||||
);
|
||||
}).toList(),
|
||||
child: TextButton.icon(
|
||||
label: Text(
|
||||
value.string(context),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.calendar_month_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onPressed: null,
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.calendar_month_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onPressed: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class InputBarWrapper extends StatefulWidget {
|
|||
|
||||
class InputBarWrapperState extends State<InputBarWrapper> {
|
||||
StreamSubscription? _choreoSub;
|
||||
String _currentText = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -61,6 +62,24 @@ class InputBarWrapperState extends State<InputBarWrapper> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
void refreshOnChange(String text) {
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(text);
|
||||
}
|
||||
|
||||
final bool decreasedFromMaxLength =
|
||||
_currentText.length >= PangeaTextController.maxLength &&
|
||||
text.length < PangeaTextController.maxLength;
|
||||
final bool reachedMaxLength =
|
||||
_currentText.length < PangeaTextController.maxLength &&
|
||||
text.length < PangeaTextController.maxLength;
|
||||
|
||||
if (decreasedFromMaxLength || reachedMaxLength) {
|
||||
setState(() {});
|
||||
}
|
||||
_currentText = text;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InputBar(
|
||||
|
|
@ -73,7 +92,7 @@ class InputBarWrapperState extends State<InputBarWrapper> {
|
|||
focusNode: widget.focusNode,
|
||||
controller: widget.controller,
|
||||
decoration: widget.decoration,
|
||||
onChanged: widget.onChanged,
|
||||
onChanged: refreshOnChange,
|
||||
autofocus: widget.autofocus,
|
||||
textInputAction: widget.textInputAction,
|
||||
readOnly: widget.readOnly,
|
||||
|
|
|
|||
|
|
@ -118,81 +118,85 @@ class OverlayMessage extends StatelessWidget {
|
|||
ownMessage: ownMessage,
|
||||
);
|
||||
|
||||
return Material(
|
||||
color: noBubble ? Colors.transparent : color,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
return Flexible(
|
||||
child: Material(
|
||||
color: noBubble ? Colors.transparent : color,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
padding: noBubble || noPadding
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: width ?? FluffyThemes.columnWidth * 1.25,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MessageContent(
|
||||
event.getDisplayEvent(timeline),
|
||||
textColor: textColor,
|
||||
borderRadius: borderRadius,
|
||||
selected: selected,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
toolbarController: toolbarController,
|
||||
isOverlay: true,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
) ||
|
||||
(pangeaMessageEvent.showUseType))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
),
|
||||
padding: noBubble || noPadding
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (pangeaMessageEvent.showUseType) ...[
|
||||
pangeaMessageEvent.msgUseType.iconView(
|
||||
context,
|
||||
textColor.withAlpha(164),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
)) ...[
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
color: textColor.withAlpha(164),
|
||||
size: 14,
|
||||
),
|
||||
Text(
|
||||
' - ${event.getDisplayEvent(timeline).originServerTs.localizedTimeShort(context)}',
|
||||
style: TextStyle(
|
||||
color: textColor.withAlpha(164),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: width ?? FluffyThemes.columnWidth * 1.25,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: MessageContent(
|
||||
event.getDisplayEvent(timeline),
|
||||
textColor: textColor,
|
||||
borderRadius: borderRadius,
|
||||
selected: selected,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
toolbarController: toolbarController,
|
||||
isOverlay: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
) ||
|
||||
(pangeaMessageEvent.showUseType))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (pangeaMessageEvent.showUseType) ...[
|
||||
pangeaMessageEvent.msgUseType.iconView(
|
||||
context,
|
||||
textColor.withAlpha(164),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
)) ...[
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
color: textColor.withAlpha(164),
|
||||
size: 14,
|
||||
),
|
||||
Text(
|
||||
' - ${event.getDisplayEvent(timeline).originServerTs.localizedTimeShort(context)}',
|
||||
style: TextStyle(
|
||||
color: textColor.withAlpha(164),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ class PangeaTextController extends TextEditingController {
|
|||
text ??= '';
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
static const int maxLength = 1000;
|
||||
bool get isMaxLength => text.length == 1000;
|
||||
|
||||
bool forceKeepOpen = false;
|
||||
|
||||
setSystemText(String text, EditType type) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue