mirror of
				https://gitlab.com/famedly/fluffychat.git
				synced 2025-11-03 22:07:23 +01:00 
			
		
		
		
	feat: Display typing indicators with gif
This commit is contained in:
		
							parent
							
								
									941d28a81d
								
							
						
					
					
						commit
						14fe60d8e0
					
				
							
								
								
									
										
											BIN
										
									
								
								assets/typing.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/typing.gif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 11 KiB  | 
							
								
								
									
										58
									
								
								lib/pages/chat/chat_app_bar_title.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								lib/pages/chat/chat_app_bar_title.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
import 'package:vrouter/vrouter.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/pages/chat/chat.dart';
 | 
			
		||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
 | 
			
		||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
 | 
			
		||||
import 'package:fluffychat/utils/room_status_extension.dart';
 | 
			
		||||
import 'package:fluffychat/utils/stream_extension.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/avatar.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/matrix.dart';
 | 
			
		||||
 | 
			
		||||
class ChatAppBarTitle extends StatelessWidget {
 | 
			
		||||
  final ChatController controller;
 | 
			
		||||
  const ChatAppBarTitle(this.controller, {Key key}) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if (controller.selectedEvents.isNotEmpty) {
 | 
			
		||||
      return Text(controller.selectedEvents.length.toString());
 | 
			
		||||
    }
 | 
			
		||||
    return ListTile(
 | 
			
		||||
      leading: Avatar(controller.room.avatar, controller.room.displayname),
 | 
			
		||||
      contentPadding: EdgeInsets.zero,
 | 
			
		||||
      onTap: controller.room.isDirectChat
 | 
			
		||||
          ? () => showModalBottomSheet(
 | 
			
		||||
                context: context,
 | 
			
		||||
                builder: (c) => UserBottomSheet(
 | 
			
		||||
                  user: controller.room
 | 
			
		||||
                      .getUserByMXIDSync(controller.room.directChatMatrixID),
 | 
			
		||||
                  outerContext: context,
 | 
			
		||||
                  onMention: () => controller.sendController.text +=
 | 
			
		||||
                      '${controller.room.getUserByMXIDSync(controller.room.directChatMatrixID).mention} ',
 | 
			
		||||
                ),
 | 
			
		||||
              )
 | 
			
		||||
          : () => VRouter.of(context)
 | 
			
		||||
              .toSegments(['rooms', controller.room.id, 'details']),
 | 
			
		||||
      title: Text(
 | 
			
		||||
          controller.room
 | 
			
		||||
              .getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
 | 
			
		||||
          maxLines: 1),
 | 
			
		||||
      subtitle: StreamBuilder<Object>(
 | 
			
		||||
        stream: Matrix.of(context)
 | 
			
		||||
            .client
 | 
			
		||||
            .onPresence
 | 
			
		||||
            .stream
 | 
			
		||||
            .where((p) => p.senderId == controller.room.directChatMatrixID)
 | 
			
		||||
            .rateLimit(const Duration(seconds: 1)),
 | 
			
		||||
        builder: (context, snapshot) => Text(
 | 
			
		||||
          controller.room.getLocalizedStatus(context),
 | 
			
		||||
          maxLines: 1,
 | 
			
		||||
          //overflow: TextOverflow.ellipsis,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -13,15 +13,14 @@ import 'package:vrouter/vrouter.dart';
 | 
			
		||||
import 'package:fluffychat/config/app_config.dart';
 | 
			
		||||
import 'package:fluffychat/config/themes.dart';
 | 
			
		||||
import 'package:fluffychat/pages/chat/chat.dart';
 | 
			
		||||
import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
 | 
			
		||||
import 'package:fluffychat/pages/chat/reactions_picker.dart';
 | 
			
		||||
import 'package:fluffychat/pages/chat/reply_display.dart';
 | 
			
		||||
import 'package:fluffychat/pages/chat/seen_by_row.dart';
 | 
			
		||||
import 'package:fluffychat/pages/chat/tombstone_display.dart';
 | 
			
		||||
import 'package:fluffychat/pages/chat/typing_indicators.dart';
 | 
			
		||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
 | 
			
		||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
 | 
			
		||||
import 'package:fluffychat/utils/platform_infos.dart';
 | 
			
		||||
import 'package:fluffychat/utils/room_status_extension.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/avatar.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/connection_status_header.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/matrix.dart';
 | 
			
		||||
@ -38,6 +37,74 @@ class ChatView extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  const ChatView(this.controller, {Key key}) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  List<Widget> _appBarActions(BuildContext context) => controller.selectMode
 | 
			
		||||
      ? [
 | 
			
		||||
          if (controller.canEditSelectedEvents)
 | 
			
		||||
            IconButton(
 | 
			
		||||
              icon: const Icon(Icons.edit_outlined),
 | 
			
		||||
              tooltip: L10n.of(context).edit,
 | 
			
		||||
              onPressed: controller.editSelectedEventAction,
 | 
			
		||||
            ),
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: const Icon(Icons.copy_outlined),
 | 
			
		||||
            tooltip: L10n.of(context).copy,
 | 
			
		||||
            onPressed: controller.copyEventsAction,
 | 
			
		||||
          ),
 | 
			
		||||
          if (controller.canRedactSelectedEvents)
 | 
			
		||||
            IconButton(
 | 
			
		||||
              icon: const Icon(Icons.delete_outlined),
 | 
			
		||||
              tooltip: L10n.of(context).redactMessage,
 | 
			
		||||
              onPressed: controller.redactEventsAction,
 | 
			
		||||
            ),
 | 
			
		||||
          if (controller.selectedEvents.length == 1)
 | 
			
		||||
            PopupMenuButton<_EventContextAction>(
 | 
			
		||||
              onSelected: (action) {
 | 
			
		||||
                switch (action) {
 | 
			
		||||
                  case _EventContextAction.info:
 | 
			
		||||
                    controller.showEventInfo();
 | 
			
		||||
                    controller.clearSelectedEvents();
 | 
			
		||||
                    break;
 | 
			
		||||
                  case _EventContextAction.report:
 | 
			
		||||
                    controller.reportEventAction();
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
              itemBuilder: (context) => [
 | 
			
		||||
                PopupMenuItem(
 | 
			
		||||
                  value: _EventContextAction.info,
 | 
			
		||||
                  child: Row(
 | 
			
		||||
                    mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      const Icon(Icons.info_outlined),
 | 
			
		||||
                      const SizedBox(width: 12),
 | 
			
		||||
                      Text(L10n.of(context).messageInfo),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                PopupMenuItem(
 | 
			
		||||
                  value: _EventContextAction.report,
 | 
			
		||||
                  child: Row(
 | 
			
		||||
                    mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      const Icon(Icons.report_outlined),
 | 
			
		||||
                      const SizedBox(width: 12),
 | 
			
		||||
                      Text(L10n.of(context).reportMessage),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
        ]
 | 
			
		||||
      : [
 | 
			
		||||
          if (controller.room.canSendDefaultStates)
 | 
			
		||||
            IconButton(
 | 
			
		||||
              tooltip: L10n.of(context).videoCall,
 | 
			
		||||
              icon: const Icon(Icons.video_call_outlined),
 | 
			
		||||
              onPressed: controller.startCallAction,
 | 
			
		||||
            ),
 | 
			
		||||
          ChatSettingsPopupMenu(controller.room, !controller.room.isDirectChat),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    controller.matrix ??= Matrix.of(context);
 | 
			
		||||
@ -87,137 +154,8 @@ class ChatView extends StatelessWidget {
 | 
			
		||||
                  )
 | 
			
		||||
                : UnreadBadgeBackButton(roomId: controller.roomId),
 | 
			
		||||
            titleSpacing: 0,
 | 
			
		||||
            title: controller.selectedEvents.isEmpty
 | 
			
		||||
                ? ListTile(
 | 
			
		||||
                    leading: Avatar(
 | 
			
		||||
                        controller.room.avatar, controller.room.displayname),
 | 
			
		||||
                    contentPadding: EdgeInsets.zero,
 | 
			
		||||
                    onTap: controller.room.isDirectChat
 | 
			
		||||
                        ? () => showModalBottomSheet(
 | 
			
		||||
                              context: context,
 | 
			
		||||
                              builder: (c) => UserBottomSheet(
 | 
			
		||||
                                user: controller.room.getUserByMXIDSync(
 | 
			
		||||
                                    controller.room.directChatMatrixID),
 | 
			
		||||
                                outerContext: context,
 | 
			
		||||
                                onMention: () => controller
 | 
			
		||||
                                        .sendController.text +=
 | 
			
		||||
                                    '${controller.room.getUserByMXIDSync(controller.room.directChatMatrixID).mention} ',
 | 
			
		||||
                              ),
 | 
			
		||||
                            )
 | 
			
		||||
                        : () => VRouter.of(context).toSegments(
 | 
			
		||||
                            ['rooms', controller.room.id, 'details']),
 | 
			
		||||
                    title: Text(
 | 
			
		||||
                        controller.room.getLocalizedDisplayname(
 | 
			
		||||
                            MatrixLocals(L10n.of(context))),
 | 
			
		||||
                        maxLines: 1),
 | 
			
		||||
                    subtitle: controller.room
 | 
			
		||||
                            .getLocalizedTypingText(context)
 | 
			
		||||
                            .isEmpty
 | 
			
		||||
                        ? StreamBuilder<Object>(
 | 
			
		||||
                            stream: Matrix.of(context)
 | 
			
		||||
                                .client
 | 
			
		||||
                                .onPresence
 | 
			
		||||
                                .stream
 | 
			
		||||
                                .where((p) =>
 | 
			
		||||
                                    p.senderId ==
 | 
			
		||||
                                    controller.room.directChatMatrixID)
 | 
			
		||||
                                .rateLimit(const Duration(seconds: 1)),
 | 
			
		||||
                            builder: (context, snapshot) => Text(
 | 
			
		||||
                                  controller.room.getLocalizedStatus(context),
 | 
			
		||||
                                  maxLines: 1,
 | 
			
		||||
                                  //overflow: TextOverflow.ellipsis,
 | 
			
		||||
                                ))
 | 
			
		||||
                        : Row(
 | 
			
		||||
                            children: <Widget>[
 | 
			
		||||
                              Icon(Icons.edit_outlined,
 | 
			
		||||
                                  color:
 | 
			
		||||
                                      Theme.of(context).colorScheme.secondary,
 | 
			
		||||
                                  size: 13),
 | 
			
		||||
                              const SizedBox(width: 4),
 | 
			
		||||
                              Expanded(
 | 
			
		||||
                                child: Text(
 | 
			
		||||
                                  controller.room
 | 
			
		||||
                                      .getLocalizedTypingText(context),
 | 
			
		||||
                                  maxLines: 1,
 | 
			
		||||
                                  style: TextStyle(
 | 
			
		||||
                                    color:
 | 
			
		||||
                                        Theme.of(context).colorScheme.secondary,
 | 
			
		||||
                                    fontStyle: FontStyle.italic,
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                            ],
 | 
			
		||||
                          ),
 | 
			
		||||
                  )
 | 
			
		||||
                : Text(controller.selectedEvents.length.toString()),
 | 
			
		||||
            actions: controller.selectMode
 | 
			
		||||
                ? <Widget>[
 | 
			
		||||
                    if (controller.canEditSelectedEvents)
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        icon: const Icon(Icons.edit_outlined),
 | 
			
		||||
                        tooltip: L10n.of(context).edit,
 | 
			
		||||
                        onPressed: controller.editSelectedEventAction,
 | 
			
		||||
                      ),
 | 
			
		||||
                    IconButton(
 | 
			
		||||
                      icon: const Icon(Icons.copy_outlined),
 | 
			
		||||
                      tooltip: L10n.of(context).copy,
 | 
			
		||||
                      onPressed: controller.copyEventsAction,
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (controller.canRedactSelectedEvents)
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        icon: const Icon(Icons.delete_outlined),
 | 
			
		||||
                        tooltip: L10n.of(context).redactMessage,
 | 
			
		||||
                        onPressed: controller.redactEventsAction,
 | 
			
		||||
                      ),
 | 
			
		||||
                    if (controller.selectedEvents.length == 1)
 | 
			
		||||
                      PopupMenuButton<_EventContextAction>(
 | 
			
		||||
                        onSelected: (action) {
 | 
			
		||||
                          switch (action) {
 | 
			
		||||
                            case _EventContextAction.info:
 | 
			
		||||
                              controller.showEventInfo();
 | 
			
		||||
                              controller.clearSelectedEvents();
 | 
			
		||||
                              break;
 | 
			
		||||
                            case _EventContextAction.report:
 | 
			
		||||
                              controller.reportEventAction();
 | 
			
		||||
                              break;
 | 
			
		||||
                          }
 | 
			
		||||
                        },
 | 
			
		||||
                        itemBuilder: (context) => [
 | 
			
		||||
                          PopupMenuItem(
 | 
			
		||||
                            value: _EventContextAction.info,
 | 
			
		||||
                            child: Row(
 | 
			
		||||
                              mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                              children: [
 | 
			
		||||
                                const Icon(Icons.info_outlined),
 | 
			
		||||
                                const SizedBox(width: 12),
 | 
			
		||||
                                Text(L10n.of(context).messageInfo),
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                          PopupMenuItem(
 | 
			
		||||
                            value: _EventContextAction.report,
 | 
			
		||||
                            child: Row(
 | 
			
		||||
                              mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                              children: [
 | 
			
		||||
                                const Icon(Icons.report_outlined),
 | 
			
		||||
                                const SizedBox(width: 12),
 | 
			
		||||
                                Text(L10n.of(context).reportMessage),
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                  ]
 | 
			
		||||
                : <Widget>[
 | 
			
		||||
                    if (controller.room.canSendDefaultStates)
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        tooltip: L10n.of(context).videoCall,
 | 
			
		||||
                        icon: const Icon(Icons.video_call_outlined),
 | 
			
		||||
                        onPressed: controller.startCallAction,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ChatSettingsPopupMenu(
 | 
			
		||||
                        controller.room, !controller.room.isDirectChat),
 | 
			
		||||
                  ],
 | 
			
		||||
            title: ChatAppBarTitle(controller),
 | 
			
		||||
            actions: _appBarActions(context),
 | 
			
		||||
          ),
 | 
			
		||||
          floatingActionButton: controller.showScrollDownButton
 | 
			
		||||
              ? Padding(
 | 
			
		||||
@ -300,7 +238,13 @@ class ChatView extends StatelessWidget {
 | 
			
		||||
                                            )
 | 
			
		||||
                                          : Container()
 | 
			
		||||
                                  : i == 0
 | 
			
		||||
                                      ? SeenByRow(controller)
 | 
			
		||||
                                      ? Column(
 | 
			
		||||
                                          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                          children: [
 | 
			
		||||
                                            SeenByRow(controller),
 | 
			
		||||
                                            TypingIndicators(controller),
 | 
			
		||||
                                          ],
 | 
			
		||||
                                        )
 | 
			
		||||
                                      : AutoScrollTag(
 | 
			
		||||
                                          key: ValueKey(controller
 | 
			
		||||
                                              .filteredEvents[i - 1].eventId),
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										98
									
								
								lib/pages/chat/typing_indicators.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								lib/pages/chat/typing_indicators.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,98 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/config/app_config.dart';
 | 
			
		||||
import 'package:fluffychat/config/themes.dart';
 | 
			
		||||
import 'package:fluffychat/pages/chat/chat.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/avatar.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/matrix.dart';
 | 
			
		||||
 | 
			
		||||
class TypingIndicators extends StatelessWidget {
 | 
			
		||||
  final ChatController controller;
 | 
			
		||||
  const TypingIndicators(this.controller, {Key key}) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final typingUsers = controller.room.typingUsers
 | 
			
		||||
      ..removeWhere((u) => u.stateKey == Matrix.of(context).client.userID);
 | 
			
		||||
    const topPadding = 24.0;
 | 
			
		||||
    const bottomPadding = 4.0;
 | 
			
		||||
 | 
			
		||||
    return Container(
 | 
			
		||||
      width: double.infinity,
 | 
			
		||||
      alignment: Alignment.center,
 | 
			
		||||
      child: AnimatedContainer(
 | 
			
		||||
        constraints:
 | 
			
		||||
            const BoxConstraints(maxWidth: FluffyThemes.columnWidth * 2.5),
 | 
			
		||||
        height: typingUsers.isEmpty
 | 
			
		||||
            ? 0
 | 
			
		||||
            : Avatar.defaultSize + bottomPadding + topPadding,
 | 
			
		||||
        duration: const Duration(milliseconds: 300),
 | 
			
		||||
        curve: Curves.bounceInOut,
 | 
			
		||||
        alignment: controller.filteredEvents.isNotEmpty &&
 | 
			
		||||
                controller.filteredEvents.first.senderId ==
 | 
			
		||||
                    Matrix.of(context).client.userID
 | 
			
		||||
            ? Alignment.topRight
 | 
			
		||||
            : Alignment.topLeft,
 | 
			
		||||
        clipBehavior: Clip.hardEdge,
 | 
			
		||||
        decoration: const BoxDecoration(),
 | 
			
		||||
        padding: EdgeInsets.only(
 | 
			
		||||
          left: typingUsers.length < 2 ? 8 : 0,
 | 
			
		||||
          bottom: bottomPadding,
 | 
			
		||||
        ),
 | 
			
		||||
        child: Row(
 | 
			
		||||
          children: [
 | 
			
		||||
            SizedBox(
 | 
			
		||||
              height: Avatar.defaultSize,
 | 
			
		||||
              width: typingUsers.length < 2
 | 
			
		||||
                  ? Avatar.defaultSize
 | 
			
		||||
                  : Avatar.defaultSize + 8,
 | 
			
		||||
              child: Stack(
 | 
			
		||||
                children: [
 | 
			
		||||
                  if (typingUsers.isNotEmpty)
 | 
			
		||||
                    Avatar(
 | 
			
		||||
                      typingUsers.first.avatarUrl,
 | 
			
		||||
                      typingUsers.first.calcDisplayname(),
 | 
			
		||||
                    ),
 | 
			
		||||
                  if (typingUsers.length == 2)
 | 
			
		||||
                    Padding(
 | 
			
		||||
                      padding: const EdgeInsets.only(left: 8),
 | 
			
		||||
                      child: Avatar(
 | 
			
		||||
                        typingUsers.length == 2
 | 
			
		||||
                            ? typingUsers.last.avatarUrl
 | 
			
		||||
                            : null,
 | 
			
		||||
                        typingUsers.length == 2
 | 
			
		||||
                            ? typingUsers.last.calcDisplayname()
 | 
			
		||||
                            : '+${typingUsers.length - 1}',
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            const SizedBox(width: 8),
 | 
			
		||||
            Padding(
 | 
			
		||||
              padding: const EdgeInsets.only(top: topPadding),
 | 
			
		||||
              child: Material(
 | 
			
		||||
                color: Theme.of(context).backgroundColor,
 | 
			
		||||
                elevation: 6,
 | 
			
		||||
                shadowColor:
 | 
			
		||||
                    Theme.of(context).secondaryHeaderColor.withAlpha(100),
 | 
			
		||||
                borderRadius: const BorderRadius.only(
 | 
			
		||||
                  topLeft: Radius.circular(2),
 | 
			
		||||
                  topRight: Radius.circular(AppConfig.borderRadius),
 | 
			
		||||
                  bottomLeft: Radius.circular(AppConfig.borderRadius),
 | 
			
		||||
                  bottomRight: Radius.circular(AppConfig.borderRadius),
 | 
			
		||||
                ),
 | 
			
		||||
                child: Padding(
 | 
			
		||||
                  padding: const EdgeInsets.all(16),
 | 
			
		||||
                  child: typingUsers.isEmpty
 | 
			
		||||
                      ? null
 | 
			
		||||
                      : Image.asset('assets/typing.gif', height: 12),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user