Skip to content

Commit

Permalink
Message Thread screen (#19)
Browse files Browse the repository at this point in the history
* design empty message screen

* merge main in current branch

* message thread screen

* show relative date

* code refactoring

* fix list ui

* rename message to thread list

* Chat screen (#30)

* design chat screen

* create separate service file for message

* fix title

* fix showing same sender name && make perfection loading in UI

* fix showing user name when time change for same sender in group thread

* manage padding for showing date header

* localise string

* manage send button color mode

* change typo userNameFirstLetter -> firstChar
  • Loading branch information
cp-ishita-g authored Jul 2, 2024
1 parent f0f696a commit e99a693
Show file tree
Hide file tree
Showing 33 changed files with 4,149 additions and 76 deletions.
3 changes: 3 additions & 0 deletions app/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
analyzer:
errors:
constant_identifier_names: ignore
include: package:flutter_lints/flutter.yaml

linter:
Expand Down
4 changes: 4 additions & 0 deletions app/assets/images/ic_send_message.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions app/assets/locales/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@
"@_COMMON": {
},
"common_get_started": "Get Started",
"common_all": "All",
"common_next": "Next",
"common_yes": "Yes",
"common_cancel": "Cancel",
"common_delete": "Delete",
"common_okay": "Okay",
"common_today": "Today",
"common_yesterday": "Yesterday",
"common_now": "Now",
"common_just_now": "Just now",
"common_min_ago": "min ago",
"common_hour_ago": "hour ago",
"common_hours_ago": "hours ago",
"common_verify": "Verify","commonMembers": "{memberCount} {memberCount, plural, =1{Member} other{Members}}",
"@commonMembers": {
"placeholders": {
Expand Down Expand Up @@ -108,6 +116,24 @@
"settings_other_option_about_us_text": "About us",
"settings_other_option_sign_out_text": "Sign out",

"message_add_member_to_send_message_title": "Add members to send messages",
"message_add_member_to_send_message_subtitle": "At least one member needs to join your space to be able to chat.",
"message_add_new_member_title": "Add a new member",
"message_tap_to_send_new_message_text": "Tap on ‘+’ to send new message to anyone in your space",
"message_delete_thread_title": "Delete thread?",
"message_delete_thread_subtitle": "Are you sure you want to delete this thread? This action cannot be undone",
"message_member_count_text": " +{memberCount}",
"@message_member_count_text": {
"placeholders": {
"memberCount": {
"type": "int"
}
}
},

"chat_start_new_chat_title": "Start new chat",
"chat_type_message_hint_text": "Type message",

"edit_profile_title": "Edit profile",
"edit_profile_first_name_title": "First name",
"edit_profile_last_name_title": "Last name",
Expand Down
184 changes: 184 additions & 0 deletions app/lib/domain/extenstions/date_formatter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import 'package:style/extenstions/date_extenstions.dart';
import 'package:yourspace_flutter/domain/extenstions/context_extenstions.dart';

enum DateFormatType {
w,
weekShort,
month,
relativeMonth,
dayMonth,
dayMonthFull,
monthYear,
monthDayYear,
dayMonthYear,
year,
time,
pastTime,
relativeTime,
relativeDate,
relativeDateWeekday,
serverIso
}

enum DateRangeFormatType {
monthYear,
relativeDate,
dayMonthYear,
}

extension DateFormatter on DateTime {
String format(BuildContext context, DateFormatType type) {
if (isUtc) return toLocal().format(context, type);

switch (type) {
case DateFormatType.w:
return DateFormat('E').format(this).substring(0, 1);
case DateFormatType.weekShort:
return DateFormat('EEE').format(this);
case DateFormatType.month:
return DateFormat('MMMM').format(this);
case DateFormatType.relativeMonth:
return year == DateTime.now().year
? format(context, DateFormatType.month)
: format(context, DateFormatType.monthYear);
case DateFormatType.dayMonth:
return DateFormat('dd MMM').format(this);
case DateFormatType.dayMonthFull:
return DateFormat('dd MMMM').format(this);
case DateFormatType.monthYear:
return DateFormat('MMMM yyyy').format(this);
case DateFormatType.monthDayYear:
return DateFormat('MMM dd, yyyy').format(this);
case DateFormatType.dayMonthYear:
return DateFormat('dd MMM yyyy').format(this);
case DateFormatType.year:
return DateFormat('yyyy').format(this);
case DateFormatType.time:
final is24HourFormat = MediaQuery.of(context).alwaysUse24HourFormat;
return DateFormat(is24HourFormat ? 'HH:mm' : 'hh:mm a')
.format(toLocal());
case DateFormatType.relativeTime:
return _relativeTime(context);
case DateFormatType.relativeDate:
return _relativeDate(context, false);
case DateFormatType.relativeDateWeekday:
return _relativeDate(context, true);
case DateFormatType.pastTime:
return _formattedPastTime(context);
case DateFormatType.serverIso:
return DateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(toUtc());
}
}

int get secondsSinceEpoch => millisecondsSinceEpoch ~/ 1000;

static String formatDateRange({
required BuildContext context,
required DateTime startDate,
required DateTime endDate,
DateRangeFormatType formatType = DateRangeFormatType.relativeDate,
}) {
if (startDate.startOfDay.isAtSameMomentAs(endDate.startOfDay)) {
if (DateRangeFormatType.relativeDate == formatType) {
return startDate.format(context, DateFormatType.relativeDate);
} else if (DateRangeFormatType.monthYear == formatType) {
return startDate.format(context, DateFormatType.monthYear);
} else {
return startDate.format(context, DateFormatType.dayMonthYear);
}
} else {
if (DateRangeFormatType.relativeDate == formatType) {
return '${startDate.format(context, DateFormatType.relativeDate)} - ${endDate.format(context, DateFormatType.relativeDate)}';
} else if (DateRangeFormatType.monthYear == formatType) {
return '${startDate.format(context, DateFormatType.monthYear)} - ${endDate.format(context, DateFormatType.monthYear)}';
} else {
if (startDate.year == endDate.year) {
return '${startDate.format(context, DateFormatType.dayMonth)} - ${endDate.format(context, DateFormatType.dayMonthYear)}';
} else {
return '${startDate.format(context, DateFormatType.dayMonthYear)} - ${endDate.format(context, DateFormatType.dayMonthYear)}';
}
}
}
}

String _relativeDate(BuildContext context, bool includeWeekday) {
final today = DateTime.now();
final yesterday = today.add(const Duration(days: -1));

if (isSameDay(today)) {
return context.l10n.common_today;
} else if (isSameDay(yesterday)) {
return context.l10n.common_yesterday;
} else if (year == today.year) {
return DateFormat('${includeWeekday ? 'EEEE, ' : ''}d MMMM').format(this);
} else {
return DateFormat('${includeWeekday ? 'EEEE, ' : ''}d MMM yyyy')
.format(this);
}
}

String _formattedPastTime(BuildContext context) {
final DateTime currentTime = DateTime.now();

if (isToday) {
final int dateSeconds = millisecondsSinceEpoch ~/ 1000;
final int currentTimeSeconds = currentTime.millisecondsSinceEpoch ~/ 1000;

if ((currentTimeSeconds - dateSeconds) < 60) {
return context.l10n.common_just_now;
} else if ((currentTimeSeconds - dateSeconds) < (60 * 60)) {
final int minutesAgo = (currentTimeSeconds - dateSeconds) ~/ 60;
return '$minutesAgo ${context.l10n.common_min_ago}';
} else {
final int hoursAgo = (currentTimeSeconds - dateSeconds) ~/ (60 * 60);

if (hoursAgo < 2) {
return '$hoursAgo ${context.l10n.common_hour_ago}';
} else {
return '$hoursAgo ${context.l10n.common_hours_ago}';
}
}
} else if (isYesterday) {
return context.l10n.common_yesterday;
} else {
final bool isSameYear = year == currentTime.year;

final String format =
isSameYear ? 'dd MMM' : 'dd MMM yyyy';
return DateFormat(format).format(this);
}
}

String _relativeTime(BuildContext context) {
final time = format(context, DateFormatType.time);

final today = DateTime.now();
final yesterday = today.add(const Duration(days: -1));

final String day;
if (isSameDay(today)) {
day = 'Today';
} else if (isSameDay(yesterday)) {
day = 'Yesterday';
} else if (year == today.year) {
day = DateFormat('d MMM').format(this);
} else {
day = DateFormat('d MMM yyyy').format(this);
}
return '$day, $time';
}

bool get isToday => startOfDay.isAtSameMomentAs(DateTime.now().startOfDay);

bool get isYesterday => startOfDay.isAtSameMomentAs(
DateTime.now().startOfDay.subtract(const Duration(days: 1)),
);
}

extension StringDateFormatter on String {
String convertTo12HrTime() => DateFormat("hh:mm a")
.format(DateFormat("HH:mm").parse(this))
.toLowerCase();
}
4 changes: 4 additions & 0 deletions app/lib/gen/assets.gen.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions app/lib/ui/app_route.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import 'dart:async';

import 'package:data/api/message/message_models.dart';
import 'package:data/api/space/space_models.dart';
import 'package:flutter/cupertino.dart';
import 'package:go_router/go_router.dart';
import 'package:yourspace_flutter/ui/flow/auth/sign_in/phone/verification/phone_verification_screen.dart';
import 'package:yourspace_flutter/ui/flow/message/chat/chat_screen.dart';
import 'package:yourspace_flutter/ui/flow/message/thread_list_screen.dart';
import 'package:yourspace_flutter/ui/flow/onboard/pick_name_screen.dart';
import 'package:yourspace_flutter/ui/flow/setting/contact_support/contact_support_screen.dart';
import 'package:yourspace_flutter/ui/flow/setting/profile/profile_screen.dart';
Expand All @@ -24,6 +30,8 @@ class AppRoute {
static const pathProfile = '/profile';
static const pathEditSpace = '/space';
static const pathContactSupport = '/contact-support';
static const pathMessage = '/message';
static const pathChat = '/chat';

final String path;
final String? name;
Expand Down Expand Up @@ -152,6 +160,19 @@ class AppRoute {
static AppRoute get contactSupport =>
AppRoute(pathContactSupport, builder: (_) => const ContactSupportScreen());

static AppRoute message(SpaceInfo space) {
return AppRoute(
pathMessage,
builder: (_) => ThreadListScreen(spaceInfo: space),
);
}
static AppRoute chat({required SpaceInfo spaceInfo, ThreadInfo? thread, List<ThreadInfo>? threadInfoList}) {
return AppRoute(
pathMessage,
builder: (_) => ChatScreen(spaceInfo: spaceInfo, threadInfo: thread, threadInfoList: threadInfoList),
);
}

static final routes = [
GoRoute(
path: intro.path,
Expand Down Expand Up @@ -220,6 +241,14 @@ class AppRoute {
path: pathContactSupport,
builder: (context, state) => state.widget(context),
),
GoRoute(
path: pathMessage,
builder: (context, state) => state.widget(context),
),
GoRoute(
path: pathChat,
builder: (context, state) => state.widget(context),
),
];
}

Expand Down
56 changes: 56 additions & 0 deletions app/lib/ui/components/profile_picture.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:style/extenstions/context_extenstions.dart';
import 'package:style/indicator/progress_indicator.dart';
import 'package:style/text/app_text_dart.dart';

class ProfileImage extends StatefulWidget {
final double size;
final String profileImageUrl;
final String firstLetter;
final TextStyle? style;
final Color? backgroundColor;

const ProfileImage({
super.key,
this.size = 64,
required this.profileImageUrl,
required this.firstLetter,
this.style,
this.backgroundColor,
});

@override
State<ProfileImage> createState() => _ProfileImageState();
}

class _ProfileImageState extends State<ProfileImage> {

@override
Widget build(BuildContext context) {
return SizedBox(
width: widget.size,
height: widget.size,
child: ClipRRect(
borderRadius: BorderRadius.circular(widget.size / 2),
child: widget.profileImageUrl.isNotEmpty
? CachedNetworkImage(
imageUrl: widget.profileImageUrl,
placeholder: (context, url) => const AppProgressIndicator(
size: AppProgressIndicatorSize.small),
errorWidget: (context, url, error) => const Icon(Icons.error),
fit: BoxFit.cover,
)
: Container(
color: widget.backgroundColor ?? context.colorScheme.containerInverseHigh,
child: Center(
child: Text(
widget.firstLetter,
style: widget.style ?? AppTextStyle.subtitle2
.copyWith(color: context.colorScheme.textPrimary)),
),
),
),
);
}
}
8 changes: 6 additions & 2 deletions app/lib/ui/flow/home/components/home_top_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ class _HomeTopBarState extends State<HomeTopBar> with TickerProviderStateMixin {
context: context,
icon: Assets.images.icMessage,
visibility: !expand,
onTap: () {},
onTap: () {
if (widget.selectedSpace != null) {
AppRoute.message(widget.selectedSpace!).push(context);
}
},
),
SizedBox(width: expand ? 0 : 8),
_iconButton(
Expand Down Expand Up @@ -165,7 +169,7 @@ class _HomeTopBarState extends State<HomeTopBar> with TickerProviderStateMixin {
.copyWith(color: context.colorScheme.textPrimary),
),
),
if (widget.fetchingInviteCode || (widget.selectedSpace == null && widget.spaces.isNotEmpty)) ...[
if (widget.fetchingInviteCode || (widget.selectedSpace == null && widget.loading)) ...[
const AppProgressIndicator(size: AppProgressIndicatorSize.small)
] else ...[
Icon(
Expand Down
Loading

0 comments on commit e99a693

Please sign in to comment.