feat(chat): 채팅방 시간 표시를 추가한다
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.chat.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import java.text.ParsePosition
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
fun formatChatRoomLastMessageTime(
|
||||||
|
context: Context,
|
||||||
|
isoText: String?,
|
||||||
|
nowMillis: Long = System.currentTimeMillis(),
|
||||||
|
timeZone: TimeZone = TimeZone.getDefault(),
|
||||||
|
locale: Locale = Locale.getDefault()
|
||||||
|
): String {
|
||||||
|
val messageMillis = parseIsoTimeMillis(isoText)
|
||||||
|
?: return context.getString(R.string.screen_chat_time_just_now)
|
||||||
|
val diffMillis = (nowMillis - messageMillis).coerceAtLeast(0L)
|
||||||
|
|
||||||
|
val minute = 60_000L
|
||||||
|
val hour = 60 * minute
|
||||||
|
val day = 24 * hour
|
||||||
|
|
||||||
|
return when {
|
||||||
|
diffMillis < minute -> context.getString(R.string.screen_chat_time_just_now)
|
||||||
|
diffMillis < hour -> context.getString(R.string.screen_chat_time_minutes, (diffMillis / minute).toInt())
|
||||||
|
diffMillis < day -> context.getString(R.string.screen_chat_time_hours, (diffMillis / hour).toInt())
|
||||||
|
diffMillis < 8 * day -> context.getString(R.string.screen_chat_time_days, (diffMillis / day).toInt())
|
||||||
|
else -> formatDateText(messageMillis, nowMillis, timeZone, locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseIsoTimeMillis(isoText: String?): Long? {
|
||||||
|
if (isoText.isNullOrBlank()) return null
|
||||||
|
|
||||||
|
val value = isoText.trim()
|
||||||
|
val patterns = listOf(
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss.SSSXX",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ssXX",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss.SSSX",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ssX"
|
||||||
|
)
|
||||||
|
|
||||||
|
for (pattern in patterns) {
|
||||||
|
val formatter = SimpleDateFormat(pattern, Locale.US).apply { isLenient = false }
|
||||||
|
val position = ParsePosition(0)
|
||||||
|
val parsed: Date? = formatter.parse(value, position)
|
||||||
|
if (parsed != null && position.index == value.length) return parsed.time
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatDateText(
|
||||||
|
messageMillis: Long,
|
||||||
|
nowMillis: Long,
|
||||||
|
timeZone: TimeZone,
|
||||||
|
locale: Locale
|
||||||
|
): String {
|
||||||
|
val nowCalendar = Calendar.getInstance(timeZone, locale).apply { timeInMillis = nowMillis }
|
||||||
|
val messageCalendar = Calendar.getInstance(timeZone, locale).apply { timeInMillis = messageMillis }
|
||||||
|
val pattern = if (nowCalendar.get(Calendar.YEAR) == messageCalendar.get(Calendar.YEAR)) {
|
||||||
|
when (locale.language) {
|
||||||
|
Locale.ENGLISH.language -> "MMM d"
|
||||||
|
Locale.JAPANESE.language -> "M月d日"
|
||||||
|
else -> "M월 d일"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"yyyy.MM.dd"
|
||||||
|
}
|
||||||
|
|
||||||
|
return SimpleDateFormat(pattern, locale).apply { this.timeZone = timeZone }.format(Date(messageMillis))
|
||||||
|
}
|
||||||
@@ -149,6 +149,10 @@
|
|||||||
<string name="tab_chat">Chat</string>
|
<string name="tab_chat">Chat</string>
|
||||||
<string name="tab_live">Live</string>
|
<string name="tab_live">Live</string>
|
||||||
<string name="tab_my">My</string>
|
<string name="tab_my">My</string>
|
||||||
|
<string name="screen_chat_time_just_now">Just now</string>
|
||||||
|
<string name="screen_chat_time_minutes">%1$d min ago</string>
|
||||||
|
<string name="screen_chat_time_hours">%1$d hours ago</string>
|
||||||
|
<string name="screen_chat_time_days">%1$d days ago</string>
|
||||||
<string name="live_now">On Air</string>
|
<string name="live_now">On Air</string>
|
||||||
<string name="screen_live_now_all_title">View all</string>
|
<string name="screen_live_now_all_title">View all</string>
|
||||||
<string name="screen_live_now_all_empty_message">There are currently no live broadcasts available, or access may be restricted due to age limits.\nVerify your identity or follow the channel to get notified when a live broadcast starts.</string>
|
<string name="screen_live_now_all_empty_message">There are currently no live broadcasts available, or access may be restricted due to age limits.\nVerify your identity or follow the channel to get notified when a live broadcast starts.</string>
|
||||||
|
|||||||
@@ -149,6 +149,10 @@
|
|||||||
<string name="tab_chat">チャット</string>
|
<string name="tab_chat">チャット</string>
|
||||||
<string name="tab_live">ライブ</string>
|
<string name="tab_live">ライブ</string>
|
||||||
<string name="tab_my">マイ</string>
|
<string name="tab_my">マイ</string>
|
||||||
|
<string name="screen_chat_time_just_now">たった今</string>
|
||||||
|
<string name="screen_chat_time_minutes">%1$d分前</string>
|
||||||
|
<string name="screen_chat_time_hours">%1$d時間前</string>
|
||||||
|
<string name="screen_chat_time_days">%1$d日前</string>
|
||||||
<string name="live_now">ただいま配信中</string>
|
<string name="live_now">ただいま配信中</string>
|
||||||
<string name="screen_live_now_all_title">配信中のライブをすべて見る</string>
|
<string name="screen_live_now_all_title">配信中のライブをすべて見る</string>
|
||||||
<string name="screen_live_now_all_empty_message">参加可能なライブがないか、年齢制限で入室できません。\n本人確認を行うか、チャンネルをフォローして\n配信通知を受け取ってみましょう。</string>
|
<string name="screen_live_now_all_empty_message">参加可能なライブがないか、年齢制限で入室できません。\n本人確認を行うか、チャンネルをフォローして\n配信通知を受け取ってみましょう。</string>
|
||||||
|
|||||||
@@ -148,6 +148,10 @@
|
|||||||
<string name="tab_chat">대화</string>
|
<string name="tab_chat">대화</string>
|
||||||
<string name="tab_live">라이브</string>
|
<string name="tab_live">라이브</string>
|
||||||
<string name="tab_my">마이</string>
|
<string name="tab_my">마이</string>
|
||||||
|
<string name="screen_chat_time_just_now">방금 전</string>
|
||||||
|
<string name="screen_chat_time_minutes">%1$d분 전</string>
|
||||||
|
<string name="screen_chat_time_hours">%1$d시간 전</string>
|
||||||
|
<string name="screen_chat_time_days">%1$d일 전</string>
|
||||||
<string name="live_now">지금 라이브 중</string>
|
<string name="live_now">지금 라이브 중</string>
|
||||||
<string name="screen_live_now_all_title">지금 라이브 중 전체보기</string>
|
<string name="screen_live_now_all_title">지금 라이브 중 전체보기</string>
|
||||||
<string name="screen_live_now_all_empty_message">현재 참여 가능한 라이브 방송이 없거나\n연령제한으로 입장이 불가능합니다.\n본인인증을 해보거나 채널을 팔로잉하고\n라이브 방송 알림을 받아보세요.</string>
|
<string name="screen_live_now_all_empty_message">현재 참여 가능한 라이브 방송이 없거나\n연령제한으로 입장이 불가능합니다.\n본인인증을 해보거나 채널을 팔로잉하고\n라이브 방송 알림을 받아보세요.</string>
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.chat
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.model.formatChatRoomLastMessageTime
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [28], application = Application::class)
|
||||||
|
class ChatRoomTimeTextFormatterTest {
|
||||||
|
|
||||||
|
private val nowMillis = 1_781_006_400_000L
|
||||||
|
private val seoulTimeZone: TimeZone = TimeZone.getTimeZone("Asia/Seoul")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `일주일 이내 시간은 상대 시간 문구로 표시한다`() {
|
||||||
|
val context = localizedContext(Locale.KOREAN)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
context.getString(R.string.screen_chat_time_just_now),
|
||||||
|
formatChatRoomLastMessageTime(context, "2026-06-09T11:59:30Z", nowMillis, seoulTimeZone, Locale.KOREAN)
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
context.getString(R.string.screen_chat_time_minutes, 3),
|
||||||
|
formatChatRoomLastMessageTime(context, "2026-06-09T11:57:00Z", nowMillis, seoulTimeZone, Locale.KOREAN)
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
context.getString(R.string.screen_chat_time_hours, 2),
|
||||||
|
formatChatRoomLastMessageTime(context, "2026-06-09T10:00:00Z", nowMillis, seoulTimeZone, Locale.KOREAN)
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
context.getString(R.string.screen_chat_time_days, 7),
|
||||||
|
formatChatRoomLastMessageTime(context, "2026-06-02T12:00:00Z", nowMillis, seoulTimeZone, Locale.KOREAN)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `일주일이 지난 같은 연도 날짜는 locale별 날짜 문구로 표시한다`() {
|
||||||
|
assertEquals(
|
||||||
|
"6월 1일",
|
||||||
|
formatChatRoomLastMessageTime(
|
||||||
|
localizedContext(Locale.KOREAN),
|
||||||
|
"2026-06-01T12:00:00Z",
|
||||||
|
nowMillis,
|
||||||
|
seoulTimeZone,
|
||||||
|
Locale.KOREAN
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"Jun 1",
|
||||||
|
formatChatRoomLastMessageTime(
|
||||||
|
localizedContext(Locale.ENGLISH),
|
||||||
|
"2026-06-01T12:00:00Z",
|
||||||
|
nowMillis,
|
||||||
|
seoulTimeZone,
|
||||||
|
Locale.ENGLISH
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"6月1日",
|
||||||
|
formatChatRoomLastMessageTime(
|
||||||
|
localizedContext(Locale.JAPANESE),
|
||||||
|
"2026-06-01T12:00:00Z",
|
||||||
|
nowMillis,
|
||||||
|
seoulTimeZone,
|
||||||
|
Locale.JAPANESE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `전년도 날짜는 yyyy 점 MM 점 dd 형식으로 표시한다`() {
|
||||||
|
assertEquals(
|
||||||
|
"2025.12.31",
|
||||||
|
formatChatRoomLastMessageTime(
|
||||||
|
localizedContext(Locale.KOREAN),
|
||||||
|
"2025-12-31T12:00:00Z",
|
||||||
|
nowMillis,
|
||||||
|
seoulTimeZone,
|
||||||
|
Locale.KOREAN
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `대상 timezone 변환 결과의 로컬 날짜를 사용한다`() {
|
||||||
|
assertEquals(
|
||||||
|
"6월 1일",
|
||||||
|
formatChatRoomLastMessageTime(
|
||||||
|
localizedContext(Locale.KOREAN),
|
||||||
|
"2026-05-31T15:30:00Z",
|
||||||
|
nowMillis,
|
||||||
|
seoulTimeZone,
|
||||||
|
Locale.KOREAN
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `분 단위 offset이 포함된 ISO 시간은 offset 전체를 반영한다`() {
|
||||||
|
val context = localizedContext(Locale.KOREAN)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
context.getString(R.string.screen_chat_time_minutes, 30),
|
||||||
|
formatChatRoomLastMessageTime(
|
||||||
|
context,
|
||||||
|
"2026-06-09T17:00:00+05:30",
|
||||||
|
nowMillis,
|
||||||
|
seoulTimeZone,
|
||||||
|
Locale.KOREAN
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `blank 또는 파싱 실패 시간은 방금 전으로 표시한다`() {
|
||||||
|
val context = localizedContext(Locale.KOREAN)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
context.getString(R.string.screen_chat_time_just_now),
|
||||||
|
formatChatRoomLastMessageTime(context, "", nowMillis, seoulTimeZone, Locale.KOREAN)
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
context.getString(R.string.screen_chat_time_just_now),
|
||||||
|
formatChatRoomLastMessageTime(context, "not-a-date", nowMillis, seoulTimeZone, Locale.KOREAN)
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
context.getString(R.string.screen_chat_time_just_now),
|
||||||
|
formatChatRoomLastMessageTime(
|
||||||
|
context,
|
||||||
|
"2026-06-09T12:00:00Zjunk",
|
||||||
|
nowMillis,
|
||||||
|
seoulTimeZone,
|
||||||
|
Locale.KOREAN
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun localizedContext(locale: Locale): Context {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
val configuration = Configuration(context.resources.configuration).apply { setLocale(locale) }
|
||||||
|
return context.createConfigurationContext(configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user