✨ Chat room scroll gradiant
This commit is contained in:
		@@ -1,4 +1,5 @@
 | 
				
			|||||||
import "dart:async";
 | 
					import "dart:async";
 | 
				
			||||||
 | 
					import "dart:math" as math;
 | 
				
			||||||
import "package:easy_localization/easy_localization.dart";
 | 
					import "package:easy_localization/easy_localization.dart";
 | 
				
			||||||
import "package:file_picker/file_picker.dart";
 | 
					import "package:file_picker/file_picker.dart";
 | 
				
			||||||
import "package:flutter/material.dart";
 | 
					import "package:flutter/material.dart";
 | 
				
			||||||
@@ -140,6 +141,9 @@ class ChatRoomScreen extends HookConsumerWidget {
 | 
				
			|||||||
    final messageController = useTextEditingController();
 | 
					    final messageController = useTextEditingController();
 | 
				
			||||||
    final scrollController = useScrollController();
 | 
					    final scrollController = useScrollController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Scroll animation notifiers
 | 
				
			||||||
 | 
					    final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final messageReplyingTo = useState<SnChatMessage?>(null);
 | 
					    final messageReplyingTo = useState<SnChatMessage?>(null);
 | 
				
			||||||
    final messageForwardingTo = useState<SnChatMessage?>(null);
 | 
					    final messageForwardingTo = useState<SnChatMessage?>(null);
 | 
				
			||||||
    final messageEditingTo = useState<SnChatMessage?>(null);
 | 
					    final messageEditingTo = useState<SnChatMessage?>(null);
 | 
				
			||||||
@@ -164,6 +168,12 @@ class ChatRoomScreen extends HookConsumerWidget {
 | 
				
			|||||||
          isLoading = true;
 | 
					          isLoading = true;
 | 
				
			||||||
          messagesNotifier.loadMore().then((_) => isLoading = false);
 | 
					          messagesNotifier.loadMore().then((_) => isLoading = false);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Update gradient animations
 | 
				
			||||||
 | 
					        final pixels = scrollController.position.pixels;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Bottom gradient: appears when not at bottom (pixels > 0)
 | 
				
			||||||
 | 
					        bottomGradientNotifier.value.value = (pixels / 500.0).clamp(0.0, 1.0);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      scrollController.addListener(onScroll);
 | 
					      scrollController.addListener(onScroll);
 | 
				
			||||||
@@ -589,7 +599,9 @@ class ChatRoomScreen extends HookConsumerWidget {
 | 
				
			|||||||
      listController: listController,
 | 
					      listController: listController,
 | 
				
			||||||
      padding: EdgeInsets.only(
 | 
					      padding: EdgeInsets.only(
 | 
				
			||||||
        top: 16,
 | 
					        top: 16,
 | 
				
			||||||
        bottom: MediaQuery.of(context).padding.bottom + 16,
 | 
					        bottom:
 | 
				
			||||||
 | 
					            MediaQuery.of(context).padding.bottom +
 | 
				
			||||||
 | 
					            80, // Leave space for chat input
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      controller: scrollController,
 | 
					      controller: scrollController,
 | 
				
			||||||
      reverse: true, // Show newest messages at the bottom
 | 
					      reverse: true, // Show newest messages at the bottom
 | 
				
			||||||
@@ -828,7 +840,7 @@ class ChatRoomScreen extends HookConsumerWidget {
 | 
				
			|||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body: Stack(
 | 
					      body: Stack(
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          // Messages and Input in Column
 | 
					          // Messages only in Column
 | 
				
			||||||
          Positioned.fill(
 | 
					          Positioned.fill(
 | 
				
			||||||
            child: Column(
 | 
					            child: Column(
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
@@ -872,73 +884,6 @@ class ChatRoomScreen extends HookConsumerWidget {
 | 
				
			|||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                if (!isSelectionMode.value)
 | 
					 | 
				
			||||||
                  chatRoom.when(
 | 
					 | 
				
			||||||
                    data:
 | 
					 | 
				
			||||||
                        (room) => Column(
 | 
					 | 
				
			||||||
                          mainAxisSize: MainAxisSize.min,
 | 
					 | 
				
			||||||
                          children: [
 | 
					 | 
				
			||||||
                            ChatInput(
 | 
					 | 
				
			||||||
                              messageController: messageController,
 | 
					 | 
				
			||||||
                              chatRoom: room!,
 | 
					 | 
				
			||||||
                              onSend: sendMessage,
 | 
					 | 
				
			||||||
                              onClear: () {
 | 
					 | 
				
			||||||
                                if (messageEditingTo.value != null) {
 | 
					 | 
				
			||||||
                                  attachments.value.clear();
 | 
					 | 
				
			||||||
                                  messageController.clear();
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                                messageEditingTo.value = null;
 | 
					 | 
				
			||||||
                                messageReplyingTo.value = null;
 | 
					 | 
				
			||||||
                                messageForwardingTo.value = null;
 | 
					 | 
				
			||||||
                              },
 | 
					 | 
				
			||||||
                              messageEditingTo: messageEditingTo.value,
 | 
					 | 
				
			||||||
                              messageReplyingTo: messageReplyingTo.value,
 | 
					 | 
				
			||||||
                              messageForwardingTo: messageForwardingTo.value,
 | 
					 | 
				
			||||||
                              onPickFile: (bool isPhoto) {
 | 
					 | 
				
			||||||
                                if (isPhoto) {
 | 
					 | 
				
			||||||
                                  pickPhotoMedia();
 | 
					 | 
				
			||||||
                                } else {
 | 
					 | 
				
			||||||
                                  pickVideoMedia();
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                              },
 | 
					 | 
				
			||||||
                              onPickAudio: pickAudioMedia,
 | 
					 | 
				
			||||||
                              onPickGeneralFile: pickGeneralFile,
 | 
					 | 
				
			||||||
                              onLinkAttachment: linkAttachment,
 | 
					 | 
				
			||||||
                              attachments: attachments.value,
 | 
					 | 
				
			||||||
                              onUploadAttachment: uploadAttachment,
 | 
					 | 
				
			||||||
                              onDeleteAttachment: (index) async {
 | 
					 | 
				
			||||||
                                final attachment = attachments.value[index];
 | 
					 | 
				
			||||||
                                if (attachment.isOnCloud &&
 | 
					 | 
				
			||||||
                                    !attachment.isLink) {
 | 
					 | 
				
			||||||
                                  final client = ref.watch(apiClientProvider);
 | 
					 | 
				
			||||||
                                  await client.delete(
 | 
					 | 
				
			||||||
                                    '/drive/files/${attachment.data.id}',
 | 
					 | 
				
			||||||
                                  );
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                                final clone = List.of(attachments.value);
 | 
					 | 
				
			||||||
                                clone.removeAt(index);
 | 
					 | 
				
			||||||
                                attachments.value = clone;
 | 
					 | 
				
			||||||
                              },
 | 
					 | 
				
			||||||
                              onMoveAttachment: (idx, delta) {
 | 
					 | 
				
			||||||
                                if (idx + delta < 0 ||
 | 
					 | 
				
			||||||
                                    idx + delta >= attachments.value.length) {
 | 
					 | 
				
			||||||
                                  return;
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                                final clone = List.of(attachments.value);
 | 
					 | 
				
			||||||
                                clone.insert(idx + delta, clone.removeAt(idx));
 | 
					 | 
				
			||||||
                                attachments.value = clone;
 | 
					 | 
				
			||||||
                              },
 | 
					 | 
				
			||||||
                              onAttachmentsChanged: (newAttachments) {
 | 
					 | 
				
			||||||
                                attachments.value = newAttachments;
 | 
					 | 
				
			||||||
                              },
 | 
					 | 
				
			||||||
                              attachmentProgress: attachmentProgress.value,
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                            Gap(MediaQuery.of(context).padding.bottom),
 | 
					 | 
				
			||||||
                          ],
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                    error: (_, _) => const SizedBox.shrink(),
 | 
					 | 
				
			||||||
                    loading: () => const SizedBox.shrink(),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
@@ -977,6 +922,112 @@ class ChatRoomScreen extends HookConsumerWidget {
 | 
				
			|||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					          // Bottom gradient - appears when scrolling towards newer messages (behind chat input)
 | 
				
			||||||
 | 
					          if (!isSelectionMode.value)
 | 
				
			||||||
 | 
					            AnimatedBuilder(
 | 
				
			||||||
 | 
					              animation: bottomGradientNotifier.value,
 | 
				
			||||||
 | 
					              builder:
 | 
				
			||||||
 | 
					                  (context, child) => Positioned(
 | 
				
			||||||
 | 
					                    left: 0,
 | 
				
			||||||
 | 
					                    right: 0,
 | 
				
			||||||
 | 
					                    bottom: 0,
 | 
				
			||||||
 | 
					                    child: Opacity(
 | 
				
			||||||
 | 
					                      opacity: bottomGradientNotifier.value.value,
 | 
				
			||||||
 | 
					                      child: Container(
 | 
				
			||||||
 | 
					                        height: math.min(
 | 
				
			||||||
 | 
					                          MediaQuery.of(context).size.height * 0.1,
 | 
				
			||||||
 | 
					                          128,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                          gradient: LinearGradient(
 | 
				
			||||||
 | 
					                            begin: Alignment.bottomCenter,
 | 
				
			||||||
 | 
					                            end: Alignment.topCenter,
 | 
				
			||||||
 | 
					                            colors: [
 | 
				
			||||||
 | 
					                              Theme.of(
 | 
				
			||||||
 | 
					                                context,
 | 
				
			||||||
 | 
					                              ).colorScheme.surfaceContainer.withOpacity(0.8),
 | 
				
			||||||
 | 
					                              Theme.of(
 | 
				
			||||||
 | 
					                                context,
 | 
				
			||||||
 | 
					                              ).colorScheme.surfaceContainer.withOpacity(0.0),
 | 
				
			||||||
 | 
					                            ],
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          // Chat Input positioned above gradient (higher z-index)
 | 
				
			||||||
 | 
					          if (!isSelectionMode.value)
 | 
				
			||||||
 | 
					            Positioned(
 | 
				
			||||||
 | 
					              left: 0,
 | 
				
			||||||
 | 
					              right: 0,
 | 
				
			||||||
 | 
					              bottom: 0, // At the very bottom, above gradient
 | 
				
			||||||
 | 
					              child: chatRoom.when(
 | 
				
			||||||
 | 
					                data:
 | 
				
			||||||
 | 
					                    (room) => Column(
 | 
				
			||||||
 | 
					                      mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        ChatInput(
 | 
				
			||||||
 | 
					                          messageController: messageController,
 | 
				
			||||||
 | 
					                          chatRoom: room!,
 | 
				
			||||||
 | 
					                          onSend: sendMessage,
 | 
				
			||||||
 | 
					                          onClear: () {
 | 
				
			||||||
 | 
					                            if (messageEditingTo.value != null) {
 | 
				
			||||||
 | 
					                              attachments.value.clear();
 | 
				
			||||||
 | 
					                              messageController.clear();
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            messageEditingTo.value = null;
 | 
				
			||||||
 | 
					                            messageReplyingTo.value = null;
 | 
				
			||||||
 | 
					                            messageForwardingTo.value = null;
 | 
				
			||||||
 | 
					                          },
 | 
				
			||||||
 | 
					                          messageEditingTo: messageEditingTo.value,
 | 
				
			||||||
 | 
					                          messageReplyingTo: messageReplyingTo.value,
 | 
				
			||||||
 | 
					                          messageForwardingTo: messageForwardingTo.value,
 | 
				
			||||||
 | 
					                          onPickFile: (bool isPhoto) {
 | 
				
			||||||
 | 
					                            if (isPhoto) {
 | 
				
			||||||
 | 
					                              pickPhotoMedia();
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                              pickVideoMedia();
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                          },
 | 
				
			||||||
 | 
					                          onPickAudio: pickAudioMedia,
 | 
				
			||||||
 | 
					                          onPickGeneralFile: pickGeneralFile,
 | 
				
			||||||
 | 
					                          onLinkAttachment: linkAttachment,
 | 
				
			||||||
 | 
					                          attachments: attachments.value,
 | 
				
			||||||
 | 
					                          onUploadAttachment: uploadAttachment,
 | 
				
			||||||
 | 
					                          onDeleteAttachment: (index) async {
 | 
				
			||||||
 | 
					                            final attachment = attachments.value[index];
 | 
				
			||||||
 | 
					                            if (attachment.isOnCloud && !attachment.isLink) {
 | 
				
			||||||
 | 
					                              final client = ref.watch(apiClientProvider);
 | 
				
			||||||
 | 
					                              await client.delete(
 | 
				
			||||||
 | 
					                                '/drive/files/${attachment.data.id}',
 | 
				
			||||||
 | 
					                              );
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            final clone = List.of(attachments.value);
 | 
				
			||||||
 | 
					                            clone.removeAt(index);
 | 
				
			||||||
 | 
					                            attachments.value = clone;
 | 
				
			||||||
 | 
					                          },
 | 
				
			||||||
 | 
					                          onMoveAttachment: (idx, delta) {
 | 
				
			||||||
 | 
					                            if (idx + delta < 0 ||
 | 
				
			||||||
 | 
					                                idx + delta >= attachments.value.length) {
 | 
				
			||||||
 | 
					                              return;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            final clone = List.of(attachments.value);
 | 
				
			||||||
 | 
					                            clone.insert(idx + delta, clone.removeAt(idx));
 | 
				
			||||||
 | 
					                            attachments.value = clone;
 | 
				
			||||||
 | 
					                          },
 | 
				
			||||||
 | 
					                          onAttachmentsChanged: (newAttachments) {
 | 
				
			||||||
 | 
					                            attachments.value = newAttachments;
 | 
				
			||||||
 | 
					                          },
 | 
				
			||||||
 | 
					                          attachmentProgress: attachmentProgress.value,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        Gap(MediaQuery.of(context).padding.bottom),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                error: (_, _) => const SizedBox.shrink(),
 | 
				
			||||||
 | 
					                loading: () => const SizedBox.shrink(),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
          // Selection mode toolbar
 | 
					          // Selection mode toolbar
 | 
				
			||||||
          if (isSelectionMode.value)
 | 
					          if (isSelectionMode.value)
 | 
				
			||||||
            Positioned(
 | 
					            Positioned(
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user