✨ 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