Skip to content

Chatview/fix chatview UI #310

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
36a19ea
test 코드 적용
Jan 12, 2025
3bf2481
Revert "test 코드 적용"
Jan 12, 2025
8ac1fad
test 반영
Jan 12, 2025
d1279e7
테스트 코드 반영
Jan 12, 2025
7e84958
Revert "테스트 코드 반영"
Jan 12, 2025
fd54230
chatbubble 테스트 코드 반영
Jan 12, 2025
645f185
chatview-ui 수정
Jan 31, 2025
84c1c90
user type UI 대응
Jan 31, 2025
159e41a
myReactionCountTextStyle
Jan 31, 2025
783f7bd
불필요한 좌우 패딩 0처리
Feb 1, 2025
602033a
loadMorea 인디케이터 opacity 애니메이션 추가
Feb 2, 2025
69d4431
custom SendMessageBuilder에 focusNode 추가
Feb 2, 2025
f52f4fb
imageMessage margin 조정
Feb 2, 2025
17d90da
로딩 관련 랜더링 퍼포먼스 개선
Jayjaejae Feb 3, 2025
9ba56a1
메시지 불필요 margin 삭제
Feb 4, 2025
aa2d5c3
이미지 메시지 세로폭 제한 옵션 cover로 변경
Feb 4, 2025
050f1f8
커스텀 메시지에도 리액션 표시
Feb 4, 2025
a9e1593
webUrl 체크 정규식 추가 & meta정보 없는 url은 파란 링크 텍스트로 표시
Feb 5, 2025
e449d93
Revert "webUrl 체크 정규식 추가 & meta정보 없는 url은 파란 링크 텍스트로 표시"
Feb 5, 2025
758823a
unreadCount value notifier 대응 및 UI 업데이트 로직 수정
Jayjaejae Feb 7, 2025
85e456d
추가 기능 반영
Jayjaejae Feb 9, 2025
6fed8ed
message reaction 처리시 customData, unreadCount 누락 추가
Feb 10, 2025
95d1989
chatview message update에 customData, unreadCount 추가
Feb 10, 2025
d786d66
링크메시지 렌더링 이슈 처리
Feb 11, 2025
c5bdebc
메시지 리스트 위젯 렌더링 퍼포먼스 개선
Jayjaejae Feb 11, 2025
0995ec6
키보드 사용 퍼포먼스 개선
Jayjaejae Feb 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions lib/src/controller/chat_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,26 @@ import 'dart:async';
import 'package:chatview/src/widgets/suggestions/suggestion_list.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:chatview/src/values/enumeration.dart';

import '../models/models.dart';

/// 메시지 업데이트를 위한 데이터 클래스
/// id별로 업데이트할 상태(status)와 (nullable) createdAt 값을 포함합니다.
class MessageUpdate {
final MessageStatus status;
final DateTime? createdAt;
final Map<String, dynamic>? customData;
final int? unreadCount;

MessageUpdate({
required this.status,
this.createdAt,
this.customData,
this.unreadCount,
});
}

class ChatController {
/// Represents initial message list in chat which can be add by user.
List<Message> initialMessageList;
Expand Down Expand Up @@ -100,6 +117,13 @@ class ChatController {
}
}

void addMessages(List<Message> messages) {
initialMessageList.addAll(messages);
if (!messageStreamController.isClosed) {
messageStreamController.sink.add(initialMessageList);
}
}

/// Used to add reply suggestions.
void addReplySuggestions(List<SuggestionItemData> suggestions) {
_replySuggestion.value = suggestions;
Expand All @@ -110,6 +134,58 @@ class ChatController {
_replySuggestion.value = [];
}

void syncMessageList(List messageList) {
initialMessageList = messageList as List<Message>;
if (!messageStreamController.isClosed) {
messageStreamController.sink.add(initialMessageList);
}
}

void deleteMessage(String messageId) {
initialMessageList.removeWhere((element) => element.id == messageId);
if (!messageStreamController.isClosed) {
messageStreamController.sink.add(initialMessageList);
}
}

/// 여러 메시지의 상태를 동시에 업데이트합니다.
///
/// [updates] 는 key가 메시지 ID(String),
/// 값이 해당 메시지의 업데이트 정보인 [MessageUpdate]인 Map입니다.
///
/// 내부적으로 리스트를 역순으로 순회하여, 해당 메시지가 존재하면 업데이트 후에 map에서 제거하며
/// map이 비면 loop를 즉시 종료하여 성능 최적화를 도모합니다.
void updateMessagesStatus(Map<String, MessageUpdate> updates) {
for (int i = initialMessageList.length - 1; i >= 0; i--) {
final message = initialMessageList[i];
if (updates.containsKey(message.id)) {
final update = updates[message.id]!;
// 메시지 상태 업데이트
message.setStatus = update.status;
// createdAt 정보가 있을 경우 업데이트
if (update.createdAt != null) {
message.setCreatedAt = update.createdAt!;
}
if (update.customData != null) {
message.customData = update.customData;
}
if (update.unreadCount != null) {
message.setUnreadCount = update.unreadCount!;
}
// 해당 업데이트 데이터 제거
updates.remove(message.id);

if (!messageStreamController.isClosed) {
messageStreamController.sink.add(initialMessageList);
}
}
// 모든 업데이트가 처리되었으면 종료
if (updates.isEmpty) {
break;
}
}
}

/// Function for setting reaction on specific chat bubble
void setReaction({
required String emoji,
Expand Down Expand Up @@ -141,6 +217,8 @@ class ChatController {
reaction: message.reaction,
messageType: message.messageType,
status: message.status,
unreadCount: message.unreadCount,
customData: message.customData,
);
if (!messageStreamController.isClosed) {
messageStreamController.sink.add(initialMessageList);
Expand Down Expand Up @@ -173,4 +251,42 @@ class ChatController {
ChatUser getUserFromId(String userId) => userId == currentUser.id
? currentUser
: otherUsers.firstWhere((element) => element.id == userId);

/// 주어진 시간 범위 내의 메시지들만 정렬합니다.
///
/// [from] 부터 [to] 까지 사이에 있는 메시지에 대해, 메시지의 [createdAt] 값을 기준으로 오름차순으로 정렬한 후
/// 해당 위치에 정렬된 순서로 업데이트합니다.
///
/// 예를 들어, 순서가 섞여있는 새로운 메시지들이 수신되었을 경우 해당 시간 구간 내에서만 정렬하여 전체 리스트에 반영합니다.
void sortMessagesByCreatedAt(DateTime from, DateTime to) {
// 해당 시간 범위에 포함되는 메시지들의 인덱스와 메시지 객체를 수집합니다.
final List<int> targetIndices = [];
final List<Message> targetMessages = [];

for (int i = 0; i < initialMessageList.length; i++) {
final message = initialMessageList[i];
// createdAt 값이 null이 아니고, from 초과 to 미만인 경우에 포함시킵니다.
if (message.createdAt != null &&
message.createdAt!.isAfter(from) &&
message.createdAt!.isBefore(to)) {
targetIndices.add(i);
targetMessages.add(message);
}
}

if (targetMessages.isEmpty) return; // 정렬 대상 메시지가 없다면 종료

// createdAt을 기준으로 오름차순 정렬 (오래된 순으로)
targetMessages.sort((a, b) => a.createdAt!.compareTo(b.createdAt!));

// 정렬된 메시지별로 원래 리스트의 해당 인덱스 위치를 업데이트합니다.
for (int j = 0; j < targetIndices.length; j++) {
initialMessageList[targetIndices[j]] = targetMessages[j];
}

// 리스트 내의 변경사항을 구독자에게 반영하기 위해 업데이트 이벤트를 발행합니다.
if (!messageStreamController.isClosed) {
messageStreamController.sink.add(initialMessageList);
}
}
}
6 changes: 6 additions & 0 deletions lib/src/extensions/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ extension ValidateString on String {

bool get isUrl => Uri.tryParse(this)?.isAbsolute ?? false;

bool get isWebUrl {
final pattern = r'^https?:\/\/[-a-zA-Z0-9@:%._\+~#=]+([\/?].*)?$';
final regExp = RegExp(pattern, caseSensitive: false);
return regExp.hasMatch(this);
}

Widget getUserProfilePicture({
required ChatUser? Function(String) getChatUser,
double? profileCircleRadius,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class MessageReactionConfiguration {
/// Used for giving text style to total count of reaction text.
final TextStyle? reactionCountTextStyle;

/// Used for giving text style to total count of reaction text.
final TextStyle? myReactionCountTextStyle;

/// Provides configurations for reaction bottom sheet which shows reacted users
/// and their reaction on any message.
final ReactionsBottomSheetConfiguration? reactionsBottomSheetConfig;
Expand All @@ -65,6 +68,7 @@ class MessageReactionConfiguration {
this.reactionsBottomSheetConfig,
this.reactionCountTextStyle,
this.reactedUserCountTextStyle,
this.myReactionCountTextStyle,
this.reactionSize,
this.margin,
this.padding,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class ProfileCircleConfiguration {
final NetworkImageProgressIndicatorBuilder?
networkImageProgressIndicatorBuilder;

// custom profile avatar
final Widget? Function(ChatUser?)? profileAvatar;

const ProfileCircleConfiguration({
this.onAvatarTap,
this.padding,
Expand All @@ -73,5 +76,6 @@ class ProfileCircleConfiguration {
this.networkImageErrorBuilder,
this.assetImageErrorBuilder,
this.networkImageProgressIndicatorBuilder,
this.profileAvatar,
});
}
2 changes: 1 addition & 1 deletion lib/src/models/config_models/receipts_widget_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ReceiptsWidgetConfig {
/// Right now it's implemented to show animation only at the last message just
/// like instagram.
/// By default [sendMessageAnimationBuilder]
final Widget Function(MessageStatus status)? receiptsBuilder;
final Widget Function(Message message)? receiptsBuilder;

/// Just like Instagram messages receipts are displayed at the bottom of last
/// message. If in case you want to modify it using your custom widget you can
Expand Down
44 changes: 44 additions & 0 deletions lib/src/models/data_models/chat_user.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,36 @@ import '../../values/enumeration.dart';
import '../../values/typedefs.dart';

class ChatUser {
static const String TYPE_USER = 'user';
static const String TYPE_ADMIN = 'admin';
static const String TYPE_BOT = 'bot';

/// Provides id of user.
final String id;

/// Provides name of user.
final String name;

// Provides title of user
final String? title;

/// Provides profile picture as network URL or asset of user.
/// Or
/// Provides profile picture's data in base64 string.
final String? profilePhoto;

/// Provides emoji of user
final String? emoji;

/// Provides introduction of user
final String? introduction;

/// Provides createdAt of user
final DateTime? createdAt;

/// Provides senderType of user
final String? type;

/// Field to set default image if network url for profile image not provided
final String defaultAvatarImage;

Expand All @@ -55,7 +74,12 @@ class ChatUser {
ChatUser({
required this.id,
required this.name,
this.title,
this.profilePhoto,
this.emoji,
this.introduction,
this.createdAt,
this.type,
this.defaultAvatarImage = profileImage,
this.imageType = ImageType.network,
this.assetImageErrorBuilder,
Expand All @@ -66,7 +90,12 @@ class ChatUser {
factory ChatUser.fromJson(Map<String, dynamic> json) => ChatUser(
id: json["id"],
name: json["name"],
title: json["title"],
profilePhoto: json["profilePhoto"],
emoji: json["emoji"],
introduction: json["introduction"],
createdAt: json["createdAt"],
type: json["type"],
imageType: ImageType.tryParse(json['imageType']?.toString()) ??
ImageType.network,
defaultAvatarImage: json["defaultAvatarImage"],
Expand All @@ -75,26 +104,41 @@ class ChatUser {
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'title': title,
'profilePhoto': profilePhoto,
'emoji': emoji,
'introduction': introduction,
'imageType': imageType.name,
'createdAt': createdAt,
'defaultAvatarImage': defaultAvatarImage,
'type': type,
};

ChatUser copyWith({
String? id,
String? name,
String? profilePhoto,
String? title,
String? emoji,
String? introduction,
ImageType? imageType,
DateTime? createdAt,
String? defaultAvatarImage,
String? type,
bool forceNullValue = false,
}) {
return ChatUser(
id: id ?? this.id,
name: name ?? this.name,
title: title ?? this.title,
emoji: emoji ?? this.emoji,
introduction: introduction ?? this.introduction,
imageType: imageType ?? this.imageType,
createdAt: createdAt ?? this.createdAt,
profilePhoto:
forceNullValue ? profilePhoto : profilePhoto ?? this.profilePhoto,
defaultAvatarImage: defaultAvatarImage ?? this.defaultAvatarImage,
type: type ?? this.type,
);
}
}
Loading