45 Commits
Author SHA1 Message Date
klaus 0fb727d0ee chore(app): versionCode를 254, 앱 버전을 1.56.3으로 올린다 2026-09-18 15:30:57 +09:00
klaus 48f2a0e543 docs(main): V2 직접 진입 리뷰를 기록한다 2026-09-18 14:49:10 +09:00
klaus c261b32148 docs(main): V2 직접 진입 문서를 정리한다 2026-09-18 14:49:00 +09:00
klaus cc8f271f44 test(chat): 대화 title bar 회귀 기준을 갱신한다 2026-09-18 14:48:54 +09:00
klaus 086af8a366 feat(main): 라이브룸 딥링크 지연 처리를 추가한다 2026-09-18 14:48:47 +09:00
klaus 5e114e18a0 feat(main): 레거시 직접 진입을 V2 메인으로 전환한다 2026-09-18 14:48:39 +09:00
klaus 330148821c chore(app): versionCode를 253으로 올린다 2026-09-17 14:45:04 +09:00
klaus 1d38912474 feat(creator): 커뮤니티 상세 작성자 채널 이동을 추가한다 2026-09-17 14:33:18 +09:00
klaus 2e67e092a5 docs(chat): 신규 DM 아이콘 변경 기록을 남긴다 2026-09-17 12:29:07 +09:00
klaus 8749ae9ae7 feat(chat): 신규 DM 버튼 아이콘을 교체한다 2026-09-17 12:29:00 +09:00
klaus 81b5a1dafc fix(chat): 대화 탭 검색 액션을 제거한다 2026-09-17 12:28:50 +09:00
klaus 582b4fe703 style(main): import 정렬을 정리한다 2026-09-17 12:28:45 +09:00
klaus 287ba0b01b chore(app): versionCode를 252로 올린다 2026-09-17 12:28:41 +09:00
klaus 7b11185db4 chore(app): versionCode를 251, 앱 버전을 1.56.2로 올린다 2026-09-15 14:12:14 +09:00
klaus 5029332023 docs(creator): 채널 공유 검증 기록을 남긴다 2026-09-15 11:46:09 +09:00
klaus f891c15eb9 test(creator): 후원 can 설정을 격리한다 2026-09-15 11:45:59 +09:00
klaus 9bb0a6512e test(common): can 테스트 helper를 추가한다 2026-09-15 11:45:51 +09:00
klaus 4bc725bdf2 feat(creator): 채널 공유 진입점을 추가한다 2026-09-15 11:45:28 +09:00
klaus 5e1a7e9ace docs(creator): 채널 공유 요구사항을 정리한다 2026-09-15 11:45:20 +09:00
klaus e57998654b docs(dm): 수신자 선택 라벨 요구사항을 기록한다 2026-09-14 22:51:55 +09:00
klaus 7c2ea3a02b fix(dm): 수신자 선택 라벨 표시를 정리한다 2026-09-14 22:51:40 +09:00
klaus a6c63b71b9 docs(chat): 수신자 선택 리뷰를 기록한다 2026-09-14 22:05:20 +09:00
klaus 4f6b235d60 feat(chat): 대화 탭 DM 시작 진입점을 추가한다 2026-09-14 22:05:12 +09:00
klaus 127e5fc147 feat(dm): 수신자 선택 화면을 연결한다 2026-09-14 22:05:05 +09:00
klaus be847b4600 feat(dm): 수신자 목록 행을 추가한다 2026-09-14 22:04:57 +09:00
klaus c6bee79f8e feat(dm): 수신자 선택 문구를 추가한다 2026-09-14 22:04:51 +09:00
klaus e9bb2c6dd8 feat(dm): 수신자 선택 상태를 추가한다 2026-09-14 22:04:45 +09:00
klaus 9de82916dd feat(dm): recipientId 계약을 추가한다 2026-09-14 22:04:40 +09:00
klaus f454596df5 docs(chat): 수신자 DM 계획을 정리한다 2026-09-14 22:04:32 +09:00
klaus 22a24cb1c1 docs(chat): 수신자 DM 요구사항을 정리한다 2026-09-14 22:04:26 +09:00
klaus 1f8d0beb2f fix(my): 배너 이미지 리소스를 정리한다 2026-08-26 17:13:11 +09:00
klaus 10886153b2 docs(creator): 채널 표시 정리 문서를 완료한다 2026-08-26 16:30:16 +09:00
klaus 1f88d721d7 feat(creator): 채널 표시와 상대 시간을 정리한다 2026-08-26 16:30:03 +09:00
klaus 9939910c71 chore(app): versionCode를 250으로 올린다 2026-08-26 16:29:55 +09:00
klaus 4f4ae62026 fix(creator): 후원 item locale 표시를 적용한다 2026-08-26 13:27:20 +09:00
klaus 3560d7bf89 chore(app): versionCode를 249로 올린다 2026-08-26 13:24:45 +09:00
klaus 5cfc42774c test(ui): 문구 변경 검증을 정리한다 2026-08-26 00:56:18 +09:00
klaus 4130ab296e fix(home): 모두 팔로우 확인 문구를 정리한다 2026-08-25 16:38:43 +09:00
klaus 87d9a1d377 fix(fanletter-write): 오른쪽 하단에 있는 글자수 단위(자) 제거 2026-08-25 16:04:43 +09:00
klaus 70f4266ed8 fix(ui): 텍스트 탭 가로 스크롤을 적용한다 2026-08-25 15:57:10 +09:00
klaus 7a9c2512cf fix(brand): 보이스온 표기를 소다라이브로 교체한다 2026-08-25 13:59:55 +09:00
klaus a04704db40 fix(content): 콘텐츠, 시리즈 오리지널 아이콘 리소스를 교체한다 2026-08-25 12:26:30 +09:00
klaus ec808cfd0c fix(home): 텍스트 로고 리소스를 교체한다 2026-08-25 12:18:51 +09:00
klaus 817234a80b fix(app): 런처 아이콘 리소스를 교체한다 2026-08-25 12:09:55 +09:00
klaus 88cb581231 fix(splash): 로고와 문구 배치를 정리한다 2026-08-25 11:49:19 +09:00
163 changed files with 5421 additions and 445 deletions
+2 -2
View File
@@ -86,8 +86,8 @@ android {
applicationId "kr.co.vividnext.sodalive" applicationId "kr.co.vividnext.sodalive"
minSdk 23 minSdk 23
targetSdk 35 targetSdk 35
versionCode 246 versionCode 254
versionName "1.56.1" versionName "1.56.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">VoiceOn-Test</string> <string name="app_name">SodaLive-Test</string>
</resources> </resources>
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">ボイスオン-テスト</string> <string name="app_name">ソーダライブ-テスト</string>
</resources> </resources>
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">보이스온-테스트</string> <string name="app_name">소다라이브-테스트</string>
</resources> </resources>
+5 -2
View File
@@ -68,6 +68,9 @@
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:replace="android:allowBackup" tools:replace="android:allowBackup"
tools:targetApi="31"> tools:targetApi="31">
<activity
android:name="kr.co.vividnext.sodalive.v2.main.chat.recipient.DmRecipientPickerActivity"
android:exported="false" />
<activity <activity
android:name=".main.DeepLinkActivity" android:name=".main.DeepLinkActivity"
android:exported="true"> android:exported="true">
@@ -174,8 +177,8 @@
<activity android:name=".settings.notification.NotificationSettingsActivity" /> <activity android:name=".settings.notification.NotificationSettingsActivity" />
<activity android:name=".settings.notification.NotificationReceiveSettingsActivity" /> <activity android:name=".settings.notification.NotificationReceiveSettingsActivity" />
<activity android:name=".settings.ContentSettingsActivity" /> <activity android:name=".settings.ContentSettingsActivity" />
<activity android:name=".live.reservation_status.LiveReservationStatusActivity" /> <activity android:name=".live.reservation.status.LiveReservationStatusActivity" />
<activity android:name=".live.reservation_status.LiveReservationCancelActivity" /> <activity android:name=".live.reservation.status.LiveReservationCancelActivity" />
<activity android:name=".audio_content.AudioContentActivity" /> <activity android:name=".audio_content.AudioContentActivity" />
<activity android:name=".audio_content.detail.AudioContentDetailActivity" /> <activity android:name=".audio_content.detail.AudioContentDetailActivity" />
<activity android:name=".audio_content.modify.AudioContentModifyActivity" /> <activity android:name=".audio_content.modify.AudioContentModifyActivity" />
@@ -60,7 +60,7 @@ class SeriesListAllActivity : BaseActivity<ActivitySeriesListAllBinding>(
if (intent.getBooleanExtra(Constants.EXTRA_IS_COMPLETED, false)) { if (intent.getBooleanExtra(Constants.EXTRA_IS_COMPLETED, false)) {
getString(R.string.screen_series_main_completed_title) getString(R.string.screen_series_main_completed_title)
} else if (intent.getBooleanExtra(Constants.EXTRA_IS_ORIGINAL, false)) { } else if (intent.getBooleanExtra(Constants.EXTRA_IS_ORIGINAL, false)) {
getString(R.string.voiceon_original_only) getString(R.string.sodalive_original_only)
} else { } else {
getString(R.string.screen_series_main_title) getString(R.string.screen_series_main_title)
} }
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅방 캐릭터 정보 모델 * 소다라이브 - 채팅방 캐릭터 정보 모델
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 모델 (서버 응답 기반) * 소다라이브 - 채팅 메시지 모델 (서버 응답 기반)
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅방 메시지 어댑터 (4.2 ViewHolder 구현) * 소다라이브 - 채팅방 메시지 어댑터 (4.2 ViewHolder 구현)
* - 4가지 ViewType 지원 * - 4가지 ViewType 지원
* - ViewHolder 바인딩 로직 구현: 시간 포맷팅, 상태 표시, 이미지 로딩, 그룹화 처리, 타이핑 애니메이션 * - ViewHolder 바인딩 로직 구현: 시간 포맷팅, 상태 표시, 이미지 로딩, 그룹화 처리, 타이핑 애니메이션
*/ */
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 Entity/도메인 매퍼 * 소다라이브 - 채팅 메시지 Entity/도메인 매퍼
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 매핑 유틸리티 * 소다라이브 - 채팅 메시지 매핑 유틸리티
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 점진적 메시지 로딩 응답 DTO * 소다라이브 - 점진적 메시지 로딩 응답 DTO
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅방 ChatRepository (Repository 패턴) * 소다라이브 - 채팅방 ChatRepository (Repository 패턴)
* - 로컬 DB(Room) + 네트워크(TalkApi) 통합 관리 * - 로컬 DB(Room) + 네트워크(TalkApi) 통합 관리
* - 통합 채팅방 입장 API 호출 및 로컬 동기화 * - 통합 채팅방 입장 API 호출 및 로컬 동기화
* - 점진적 메시지 로딩 * - 점진적 메시지 로딩
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 통합 채팅방 입장 응답 DTO * 소다라이브 - 통합 채팅방 입장 응답 DTO
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅방 메시지 전송 상태 Enum * 소다라이브 - 채팅방 메시지 전송 상태 Enum
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅방 메시지 타입 Enum * 소다라이브 - 채팅방 메시지 타입 Enum
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 모델 (서버 응답 전용 DTO) * 소다라이브 - 채팅 메시지 모델 (서버 응답 전용 DTO)
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 Room TypeConverters * 소다라이브 - 채팅 메시지 Room TypeConverters
*/ */
package kr.co.vividnext.sodalive.chat.talk.room.db package kr.co.vividnext.sodalive.chat.talk.room.db
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 DAO * 소다라이브 - 채팅 메시지 DAO
*/ */
package kr.co.vividnext.sodalive.chat.talk.room.db package kr.co.vividnext.sodalive.chat.talk.room.db
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅방 Room Database (ChatMessage 테이블 포함) * 소다라이브 - 채팅방 Room Database (ChatMessage 테이블 포함)
*/ */
package kr.co.vividnext.sodalive.chat.talk.room.db package kr.co.vividnext.sodalive.chat.talk.room.db
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 Room Entity * 소다라이브 - 채팅 메시지 Room Entity
*/ */
package kr.co.vividnext.sodalive.chat.talk.room.db package kr.co.vividnext.sodalive.chat.talk.room.db
@@ -92,6 +92,28 @@ object SharedPreferenceManager {
} }
} }
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun resetCanForTest() {
ensureInitialized()
runBlocking {
dataStore.edit { preferences ->
removeByName(preferences, Constants.PREF_CAN)
}
}
preferenceState.update { state -> state - Constants.PREF_CAN }
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun setCanForTest(value: Int) {
ensureInitialized()
runBlocking {
dataStore.edit { preferences ->
preferences[intPreferencesKey(Constants.PREF_CAN)] = value
}
}
preferenceState.update { state -> state + (Constants.PREF_CAN to value) }
}
fun clear() { fun clear() {
ensureInitialized() ensureInitialized()
@@ -96,7 +96,7 @@ import kr.co.vividnext.sodalive.live.LiveRepository
import kr.co.vividnext.sodalive.live.LiveViewModel import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendApi import kr.co.vividnext.sodalive.live.recommend.LiveRecommendApi
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepository import kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepository
import kr.co.vividnext.sodalive.live.reservation_status.LiveReservationStatusViewModel import kr.co.vividnext.sodalive.live.reservation.status.LiveReservationStatusViewModel
import kr.co.vividnext.sodalive.live.room.LiveRoomViewModel import kr.co.vividnext.sodalive.live.room.LiveRoomViewModel
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailViewModel import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailViewModel
@@ -463,13 +463,13 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { CreatorChannelLiveViewModel(get()) } viewModel { CreatorChannelLiveViewModel(get()) }
viewModel { CreatorChannelAudioViewModel(get()) } viewModel { CreatorChannelAudioViewModel(get()) }
viewModel { CreatorChannelSeriesViewModel(get()) } viewModel { CreatorChannelSeriesViewModel(get()) }
viewModel { CreatorChannelCommunityViewModel(get(), get()) } viewModel { CreatorChannelCommunityViewModel(get()) }
viewModel { CreatorChannelCommunityDetailViewModel(get(), get(), get()) } viewModel { CreatorChannelCommunityDetailViewModel(get(), get()) }
viewModel { CreatorChannelCommunityReplyViewModel(get(), get(), get()) } viewModel { CreatorChannelCommunityReplyViewModel(get(), get()) }
viewModel { CreatorChannelFanTalkViewModel(get(), get()) } viewModel { CreatorChannelFanTalkViewModel(get()) }
viewModel { CreatorChannelFanTalkDetailViewModel(get(), get()) } viewModel { CreatorChannelFanTalkDetailViewModel(get()) }
viewModel { CreatorChannelFanTalkWriteViewModel(get()) } viewModel { CreatorChannelFanTalkWriteViewModel(get()) }
viewModel { CreatorChannelDonationViewModel(get(), get()) } viewModel { CreatorChannelDonationViewModel(get()) }
viewModel { PushNotificationListViewModel(get()) } viewModel { PushNotificationListViewModel(get()) }
viewModel { CharacterTabViewModel(get()) } viewModel { CharacterTabViewModel(get()) }
viewModel { CharacterDetailViewModel(get()) } viewModel { CharacterDetailViewModel(get()) }
@@ -359,7 +359,7 @@ class UserProfileViewModel(
"deep_link_sub5" to "$userId" "deep_link_sub5" to "$userId"
) )
val shareUrl = Utils.createOneLinkUrl(params = params) val shareUrl = Utils.createOneLinkUrl(params = params)
onSuccess("보이스온 ${nickname}님의 채널입니다.\n$shareUrl") onSuccess("소다라이브 ${nickname}님의 채널입니다.\n$shareUrl")
} }
fun getChannelDonationList(creatorId: Long) { fun getChannelDonationList(creatorId: Long) {
@@ -604,7 +604,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
recyclerView.adapter = originalSeriesAdapter recyclerView.adapter = originalSeriesAdapter
binding.tvSeriesOriginalAll.setOnClickListener { binding.tvSeriesOriginalAll.setOnClickListener {
// ‘오직 보이스온에서만’ 전체보기: isOriginal=true로 시리즈 전체보기 화면 진입 // ‘오직 소다라이브에서만’ 전체보기: isOriginal=true로 시리즈 전체보기 화면 진입
if (SharedPreferenceManager.token.isNotBlank()) { if (SharedPreferenceManager.token.isNotBlank()) {
startActivity( startActivity(
Intent( Intent(
@@ -5,8 +5,8 @@ import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest
import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse
import kr.co.vividnext.sodalive.live.reservation_status.CancelLiveReservationRequest import kr.co.vividnext.sodalive.live.reservation.status.CancelLiveReservationRequest
import kr.co.vividnext.sodalive.live.reservation_status.GetLiveReservationResponse import kr.co.vividnext.sodalive.live.reservation.status.GetLiveReservationResponse
import kr.co.vividnext.sodalive.live.room.CancelLiveRequest import kr.co.vividnext.sodalive.live.room.CancelLiveRequest
import kr.co.vividnext.sodalive.live.room.EnterOrQuitLiveRoomRequest import kr.co.vividnext.sodalive.live.room.EnterOrQuitLiveRoomRequest
import kr.co.vividnext.sodalive.live.room.LiveRoomStatus import kr.co.vividnext.sodalive.live.room.LiveRoomStatus
@@ -4,7 +4,7 @@ import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest
import kr.co.vividnext.sodalive.live.reservation_status.CancelLiveReservationRequest import kr.co.vividnext.sodalive.live.reservation.status.CancelLiveReservationRequest
import kr.co.vividnext.sodalive.live.room.CancelLiveRequest import kr.co.vividnext.sodalive.live.room.CancelLiveRequest
import kr.co.vividnext.sodalive.live.room.EnterOrQuitLiveRoomRequest import kr.co.vividnext.sodalive.live.room.EnterOrQuitLiveRoomRequest
import kr.co.vividnext.sodalive.live.room.LiveRoomStatus import kr.co.vividnext.sodalive.live.room.LiveRoomStatus
@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.live.reservation_status package kr.co.vividnext.sodalive.live.reservation.status
import androidx.annotation.Keep import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.live.reservation_status package kr.co.vividnext.sodalive.live.reservation.status
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.Keep import androidx.annotation.Keep
@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.live.reservation_status package kr.co.vividnext.sodalive.live.reservation.status
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
@@ -18,10 +18,10 @@ import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.databinding.ActivityLiveReservationCancelBinding import kr.co.vividnext.sodalive.databinding.ActivityLiveReservationCancelBinding
import kr.co.vividnext.sodalive.extensions.convertDateFormat import kr.co.vividnext.sodalive.extensions.convertDateFormat
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity
import kr.co.vividnext.sodalive.settings.language.LanguageManager import kr.co.vividnext.sodalive.settings.language.LanguageManager
import kr.co.vividnext.sodalive.settings.language.LocaleHelper import kr.co.vividnext.sodalive.settings.language.LocaleHelper
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
@@ -117,7 +117,7 @@ class LiveReservationCancelActivity : BaseActivity<ActivityLiveReservationCancel
binding.tvOtherSudaReservation.setOnClickListener { binding.tvOtherSudaReservation.setOnClickListener {
finishAffinity() finishAffinity()
startActivity(Intent(applicationContext, MainActivity::class.java)) startActivity(Intent(applicationContext, MainV2Activity::class.java))
} }
binding.tvCheckCanStatus.setOnClickListener { binding.tvCheckCanStatus.setOnClickListener {
@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.live.reservation_status package kr.co.vividnext.sodalive.live.reservation.status
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.live.reservation_status package kr.co.vividnext.sodalive.live.reservation.status
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.live.reservation_status package kr.co.vividnext.sodalive.live.reservation.status
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -120,13 +120,13 @@ import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewDialog
import kr.co.vividnext.sodalive.live.roulette.RouletteSpinDialog import kr.co.vividnext.sodalive.live.roulette.RouletteSpinDialog
import kr.co.vividnext.sodalive.live.roulette.config.RouletteConfigActivity import kr.co.vividnext.sodalive.live.roulette.config.RouletteConfigActivity
import kr.co.vividnext.sodalive.main.DeepLinkActivity import kr.co.vividnext.sodalive.main.DeepLinkActivity
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.report.ProfileReportDialog import kr.co.vividnext.sodalive.report.ProfileReportDialog
import kr.co.vividnext.sodalive.report.ReportType import kr.co.vividnext.sodalive.report.ReportType
import kr.co.vividnext.sodalive.report.UserReportDialog import kr.co.vividnext.sodalive.report.UserReportDialog
import kr.co.vividnext.sodalive.settings.ContentSettingsActivity import kr.co.vividnext.sodalive.settings.ContentSettingsActivity
import kr.co.vividnext.sodalive.settings.language.LanguageManager import kr.co.vividnext.sodalive.settings.language.LanguageManager
import kr.co.vividnext.sodalive.settings.notification.MemberRole import kr.co.vividnext.sodalive.settings.notification.MemberRole
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import org.json.JSONObject import org.json.JSONObject
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -379,7 +379,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
val bundle = intent?.getBundleExtra(Constants.EXTRA_DATA) ?: return val bundle = intent?.getBundleExtra(Constants.EXTRA_DATA) ?: return
showDeepLinkNavigationDialog { showDeepLinkNavigationDialog {
val nextIntent = Intent(applicationContext, MainActivity::class.java).apply { val nextIntent = Intent(applicationContext, MainV2Activity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
putExtra(Constants.EXTRA_DATA, bundle) putExtra(Constants.EXTRA_DATA, bundle)
} }
@@ -436,12 +436,12 @@ class LiveRoomViewModel(
val message = if (isPrivateRoom) { val message = if (isPrivateRoom) {
"${SharedPreferenceManager.nickname}님이 귀하를 " + "${SharedPreferenceManager.nickname}님이 귀하를 " +
"보이스온의 비공개라이브에 초대하였습니다.\n" + "소다라이브의 비공개라이브에 초대하였습니다.\n" +
"※ 라이브 참여: $shareUrl\n" + "※ 라이브 참여: $shareUrl\n" +
"(입장 비밀번호 : $password)" "(입장 비밀번호 : $password)"
} else { } else {
"${SharedPreferenceManager.nickname}님이 귀하를 " + "${SharedPreferenceManager.nickname}님이 귀하를 " +
"보이스온의 공개라이브에 초대하였습니다.\n" + "소다라이브의 공개라이브에 초대하였습니다.\n" +
"※ 라이브 참여: $shareUrl" "※ 라이브 참여: $shareUrl"
} }
@@ -183,7 +183,7 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
} }
} }
binding.ivIntroduceVoiceon.setOnClickListener { binding.ivIntroduceSodalive.setOnClickListener {
val url = "https://blog.naver.com/sodalive_official" val url = "https://blog.naver.com/sodalive_official"
if (URLUtil.isValidUrl(url)) { if (URLUtil.isValidUrl(url)) {
startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
@@ -30,6 +30,7 @@ import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.Utils
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelBinding import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog
@@ -230,6 +231,7 @@ class CreatorChannelActivity :
private fun setupClickListeners() { private fun setupClickListeners() {
binding.ivBack.setOnClickListener { finish() } binding.ivBack.setOnClickListener { finish() }
binding.ivMore.setOnClickListener { onMoreClicked() } binding.ivMore.setOnClickListener { onMoreClicked() }
binding.ivShare.setOnClickListener { shareCurrentCreatorChannel() }
binding.tvFollowerCount.setOnClickListener { onFollowerCountClicked() } binding.tvFollowerCount.setOnClickListener { onFollowerCountClicked() }
binding.layoutFollowCapsule.setOnClickListener { onFollowCapsuleClicked() } binding.layoutFollowCapsule.setOnClickListener { onFollowCapsuleClicked() }
binding.ivBell.setOnClickListener { onBellClicked() } binding.ivBell.setOnClickListener { onBellClicked() }
@@ -291,9 +293,11 @@ class CreatorChannelActivity :
binding.layoutFollowCapsule.visibility = View.GONE binding.layoutFollowCapsule.visibility = View.GONE
binding.ivBell.visibility = View.GONE binding.ivBell.visibility = View.GONE
binding.ivMore.visibility = View.GONE binding.ivMore.visibility = View.GONE
binding.ivShare.visibility = View.VISIBLE
return return
} }
binding.ivShare.visibility = View.GONE
binding.layoutFollowCapsule.visibility = View.VISIBLE binding.layoutFollowCapsule.visibility = View.VISIBLE
binding.ivMore.visibility = View.VISIBLE binding.ivMore.visibility = View.VISIBLE
val titleBarState = CreatorChannelTitleBarState.from( val titleBarState = CreatorChannelTitleBarState.from(
@@ -435,12 +439,35 @@ class CreatorChannelActivity :
if (header.isOwner) return if (header.isOwner) return
CreatorChannelMoreBottomSheet.newInstance().apply { CreatorChannelMoreBottomSheet.newInstance().apply {
onClickChannelShare = { shareCurrentCreatorChannel() }
onClickBlock = { showUserBlockDialog() } onClickBlock = { showUserBlockDialog() }
onClickUserReport = { showUserReportDialog() } onClickUserReport = { showUserReportDialog() }
onClickProfileReport = { showProfileReportDialog() } onClickProfileReport = { showProfileReportDialog() }
}.show(supportFragmentManager, CreatorChannelMoreBottomSheet::class.java.simpleName) }.show(supportFragmentManager, CreatorChannelMoreBottomSheet::class.java.simpleName)
} }
private fun shareCurrentCreatorChannel() {
val header = currentHeader ?: return
if (creatorId <= 0L) return
val nickname = header.nickname
val params = mapOf(
"af_dp" to "voiceon://",
"deep_link_value" to "channel",
"deep_link_sub5" to "$creatorId"
)
val shareUrl = Utils.createOneLinkUrl(params = params)
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, "소다라이브 ${nickname}님의 채널입니다.\n$shareUrl")
val shareIntent = Intent.createChooser(
intent,
getString(R.string.screen_user_profile_share_channel)
)
startActivity(shareIntent)
}
private fun showUserBlockDialog() { private fun showUserBlockDialog() {
val nickname = currentHeader?.nickname.orEmpty() val nickname = currentHeader?.nickname.orEmpty()
V2ModalDialog( V2ModalDialog(
@@ -9,6 +9,7 @@ import kr.co.vividnext.sodalive.databinding.DialogCreatorChannelMoreBinding
class CreatorChannelMoreBottomSheet : BottomSheetDialogFragment() { class CreatorChannelMoreBottomSheet : BottomSheetDialogFragment() {
var onClickChannelShare: (() -> Unit)? = null
var onClickBlock: (() -> Unit)? = null var onClickBlock: (() -> Unit)? = null
var onClickUserReport: (() -> Unit)? = null var onClickUserReport: (() -> Unit)? = null
var onClickProfileReport: (() -> Unit)? = null var onClickProfileReport: (() -> Unit)? = null
@@ -29,6 +30,10 @@ class CreatorChannelMoreBottomSheet : BottomSheetDialogFragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val viewBinding = binding ?: return val viewBinding = binding ?: return
viewBinding.tvChannelShare.setOnClickListener {
dismiss()
onClickChannelShare?.invoke()
}
viewBinding.tvUserBlock.setOnClickListener { viewBinding.tvUserBlock.setOnClickListener {
dismiss() dismiss()
onClickBlock?.invoke() onClickBlock?.invoke()
@@ -7,15 +7,13 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.toCommunityPostUiModels import kr.co.vividnext.sodalive.v2.creator.channel.community.model.toCommunityPostUiModels
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
class CreatorChannelCommunityViewModel( class CreatorChannelCommunityViewModel(
private val repository: CreatorChannelRepository, private val repository: CreatorChannelRepository
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() { ) : BaseViewModel() {
private val _communityStateLiveData = MutableLiveData<CreatorChannelCommunityUiState>() private val _communityStateLiveData = MutableLiveData<CreatorChannelCommunityUiState>()
@@ -158,7 +156,6 @@ class CreatorChannelCommunityViewModel(
private fun CreatorChannelCommunityTabResponse.toCommunityPostUiModels(): List<CreatorChannelCommunityPostUiModel> = private fun CreatorChannelCommunityTabResponse.toCommunityPostUiModels(): List<CreatorChannelCommunityPostUiModel> =
communityPosts.toCommunityPostUiModels( communityPosts.toCommunityPostUiModels(
relativeTimeTextFormatter = relativeTimeTextFormatter,
isOwner = isOwner, isOwner = isOwner,
currentUserId = SharedPreferenceManager.userId currentUserId = SharedPreferenceManager.userId
) )
@@ -24,6 +24,7 @@ import com.bumptech.glide.request.target.Target
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityReportDialog import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityReportDialog
@@ -34,6 +35,7 @@ import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.Creato
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyActivity import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyActivity
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui.CreatorChannelCommunityCommentAdapter import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui.CreatorChannelCommunityCommentAdapter
import kr.co.vividnext.sodalive.v2.widget.feed.calculateFeedCommunityImageHeight import kr.co.vividnext.sodalive.v2.widget.feed.calculateFeedCommunityImageHeight
@@ -171,7 +173,13 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
tvCreatorChannelCommunityDetailNickname.text = post.creatorNickname tvCreatorChannelCommunityDetailNickname.text = post.creatorNickname
tvCreatorChannelCommunityDetailTime.text = post.createdAtText tvCreatorChannelCommunityDetailTime.text =
formatUtcRelativeTimeText(tvCreatorChannelCommunityDetailTime.context, post.createdAtUtc)
layoutCreatorChannelCommunityDetailCreator.setOnClickListener {
startActivity(
CreatorChannelActivity.newIntent(this@CreatorChannelCommunityDetailActivity, post.creatorId)
)
}
btnCreatorChannelCommunityDetailMore.isVisible = post.showMore btnCreatorChannelCommunityDetailMore.isVisible = post.showMore
tvCreatorChannelCommunityDetailBody.text = post.content tvCreatorChannelCommunityDetailBody.text = post.content
layoutCreatorChannelCommunityDetailReaction.isVisible = post.showReaction || content.isCommentAvailable layoutCreatorChannelCommunityDetailReaction.isVisible = post.showReaction || content.isCommentAvailable
@@ -11,7 +11,6 @@ import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.ModifyCommunityPostRequest import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.ModifyCommunityPostRequest
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent
@@ -25,8 +24,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
class CreatorChannelCommunityDetailViewModel( class CreatorChannelCommunityDetailViewModel(
private val repository: CreatorChannelRepository, private val repository: CreatorChannelRepository,
private val legacyRepository: CreatorCommunityRepository, private val legacyRepository: CreatorCommunityRepository
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() { ) : BaseViewModel() {
private val _detailStateLiveData = MutableLiveData<CreatorChannelCommunityDetailUiState>() private val _detailStateLiveData = MutableLiveData<CreatorChannelCommunityDetailUiState>()
@@ -536,7 +534,7 @@ class CreatorChannelCommunityDetailViewModel(
creatorId = creatorId, creatorId = creatorId,
creatorNickname = creatorNickname, creatorNickname = creatorNickname,
creatorProfileUrl = creatorProfileUrl, creatorProfileUrl = creatorProfileUrl,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc), createdAtUtc = createdAtUtc,
content = content, content = content,
imageUrl = imageUrl.takeUnless { isLocked() }, imageUrl = imageUrl.takeUnless { isLocked() },
audioUrl = audioUrl.takeUnless { isLocked() }, audioUrl = audioUrl.takeUnless { isLocked() },
@@ -566,7 +564,6 @@ class CreatorChannelCommunityDetailViewModel(
memberNickname = writerNickname, memberNickname = writerNickname,
memberProfileUrl = writerProfileImageUrl, memberProfileUrl = writerProfileImageUrl,
createdAtUtc = createdAtUtc, createdAtUtc = createdAtUtc,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
comment = content, comment = content,
likeCount = 0, likeCount = 0,
replyCount = if (latestReply != null) 1 else 0, replyCount = if (latestReply != null) 1 else 0,
@@ -586,7 +583,6 @@ class CreatorChannelCommunityDetailViewModel(
memberNickname = writerNickname, memberNickname = writerNickname,
memberProfileUrl = writerProfileImageUrl, memberProfileUrl = writerProfileImageUrl,
createdAtUtc = createdAtUtc, createdAtUtc = createdAtUtc,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
comment = content, comment = content,
likeCount = 0, likeCount = 0,
isLiked = false, isLiked = false,
@@ -625,7 +621,7 @@ data class CreatorChannelCommunityPostDetailUiModel(
val creatorId: Long, val creatorId: Long,
val creatorNickname: String, val creatorNickname: String,
val creatorProfileUrl: String?, val creatorProfileUrl: String?,
val createdAtText: String, val createdAtUtc: String,
val content: String, val content: String,
val imageUrl: String?, val imageUrl: String?,
val audioUrl: String?, val audioUrl: String?,
@@ -648,7 +644,6 @@ data class CreatorChannelCommunityCommentUiModel(
val memberNickname: String, val memberNickname: String,
val memberProfileUrl: String?, val memberProfileUrl: String?,
val createdAtUtc: String, val createdAtUtc: String,
val createdAtText: String,
val comment: String, val comment: String,
val likeCount: Int, val likeCount: Int,
val replyCount: Int, val replyCount: Int,
@@ -665,7 +660,6 @@ data class CreatorChannelCommunityReplyUiModel(
val memberNickname: String, val memberNickname: String,
val memberProfileUrl: String?, val memberProfileUrl: String?,
val createdAtUtc: String, val createdAtUtc: String,
val createdAtText: String,
val comment: String, val comment: String,
val likeCount: Int, val likeCount: Int,
val isLiked: Boolean, val isLiked: Boolean,
@@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityReplyBinding import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityReplyBinding
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
@@ -110,6 +111,11 @@ class CreatorChannelCommunityReplyActivity : BaseActivity<ActivityCreatorChannel
onModifyClick = { comment -> viewModel.startParentCommentEdit(comment.comment) }, onModifyClick = { comment -> viewModel.startParentCommentEdit(comment.comment) },
onDeleteClick = { showDeleteParentCommentDialog() } onDeleteClick = { showDeleteParentCommentDialog() }
) )
layoutCreatorChannelCommunityReplyParent.tvCreatorChannelCommunityCommentTime.text =
formatUtcRelativeTimeText(
layoutCreatorChannelCommunityReplyParent.tvCreatorChannelCommunityCommentTime.context,
content.parentComment.createdAtUtc
)
layoutCreatorChannelCommunityReplyParent.layoutCreatorChannelCommunityCommentLatestReply.isVisible = false layoutCreatorChannelCommunityReplyParent.layoutCreatorChannelCommunityCommentLatestReply.isVisible = false
replyAdapter.submitItems(content.replies) replyAdapter.submitItems(content.replies)
if (etCreatorChannelCommunityReply.text.toString() != content.replyInput) { if (etCreatorChannelCommunityReply.text.toString() != content.replyInput) {
@@ -183,7 +189,6 @@ class CreatorChannelCommunityReplyActivity : BaseActivity<ActivityCreatorChannel
memberNickname = intent.getStringExtra(EXTRA_MEMBER_NICKNAME).orEmpty(), memberNickname = intent.getStringExtra(EXTRA_MEMBER_NICKNAME).orEmpty(),
memberProfileUrl = intent.getStringExtra(EXTRA_MEMBER_PROFILE_URL), memberProfileUrl = intent.getStringExtra(EXTRA_MEMBER_PROFILE_URL),
createdAtUtc = intent.getStringExtra(EXTRA_CREATED_AT_UTC).orEmpty(), createdAtUtc = intent.getStringExtra(EXTRA_CREATED_AT_UTC).orEmpty(),
createdAtText = intent.getStringExtra(EXTRA_CREATED_AT_TEXT).orEmpty(),
comment = intent.getStringExtra(EXTRA_COMMENT).orEmpty(), comment = intent.getStringExtra(EXTRA_COMMENT).orEmpty(),
likeCount = intent.getIntExtra(EXTRA_LIKE_COUNT, 0), likeCount = intent.getIntExtra(EXTRA_LIKE_COUNT, 0),
replyCount = intent.getIntExtra(EXTRA_REPLY_COUNT, 0), replyCount = intent.getIntExtra(EXTRA_REPLY_COUNT, 0),
@@ -201,7 +206,6 @@ class CreatorChannelCommunityReplyActivity : BaseActivity<ActivityCreatorChannel
private const val EXTRA_MEMBER_NICKNAME = "extra_member_nickname" private const val EXTRA_MEMBER_NICKNAME = "extra_member_nickname"
private const val EXTRA_MEMBER_PROFILE_URL = "extra_member_profile_url" private const val EXTRA_MEMBER_PROFILE_URL = "extra_member_profile_url"
private const val EXTRA_CREATED_AT_UTC = "extra_created_at_utc" private const val EXTRA_CREATED_AT_UTC = "extra_created_at_utc"
private const val EXTRA_CREATED_AT_TEXT = "extra_created_at_text"
private const val EXTRA_COMMENT = "extra_comment" private const val EXTRA_COMMENT = "extra_comment"
private const val EXTRA_LIKE_COUNT = "extra_like_count" private const val EXTRA_LIKE_COUNT = "extra_like_count"
private const val EXTRA_REPLY_COUNT = "extra_reply_count" private const val EXTRA_REPLY_COUNT = "extra_reply_count"
@@ -217,7 +221,6 @@ class CreatorChannelCommunityReplyActivity : BaseActivity<ActivityCreatorChannel
.putExtra(EXTRA_MEMBER_NICKNAME, parentComment.memberNickname) .putExtra(EXTRA_MEMBER_NICKNAME, parentComment.memberNickname)
.putExtra(EXTRA_MEMBER_PROFILE_URL, parentComment.memberProfileUrl) .putExtra(EXTRA_MEMBER_PROFILE_URL, parentComment.memberProfileUrl)
.putExtra(EXTRA_CREATED_AT_UTC, parentComment.createdAtUtc) .putExtra(EXTRA_CREATED_AT_UTC, parentComment.createdAtUtc)
.putExtra(EXTRA_CREATED_AT_TEXT, parentComment.createdAtText)
.putExtra(EXTRA_COMMENT, parentComment.comment) .putExtra(EXTRA_COMMENT, parentComment.comment)
.putExtra(EXTRA_LIKE_COUNT, parentComment.likeCount) .putExtra(EXTRA_LIKE_COUNT, parentComment.likeCount)
.putExtra(EXTRA_REPLY_COUNT, parentComment.replyCount) .putExtra(EXTRA_REPLY_COUNT, parentComment.replyCount)
@@ -10,7 +10,6 @@ import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
@@ -21,8 +20,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
class CreatorChannelCommunityReplyViewModel( class CreatorChannelCommunityReplyViewModel(
private val repository: CreatorChannelRepository, private val repository: CreatorChannelRepository,
private val legacyRepository: CreatorCommunityRepository, private val legacyRepository: CreatorCommunityRepository
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() { ) : BaseViewModel() {
private val _replyStateLiveData = MutableLiveData<CreatorChannelCommunityReplyUiState>() private val _replyStateLiveData = MutableLiveData<CreatorChannelCommunityReplyUiState>()
@@ -57,20 +55,17 @@ class CreatorChannelCommunityReplyViewModel(
private var isModifyingReply = false private var isModifyingReply = false
fun loadReplies(postId: Long, parentComment: CreatorChannelCommunityCommentUiModel) { fun loadReplies(postId: Long, parentComment: CreatorChannelCommunityCommentUiModel) {
val formattedParentComment = parentComment.copy(
createdAtText = relativeTimeTextFormatter.format(parentComment.createdAtUtc)
)
this.postId = postId this.postId = postId
this.parentComment = formattedParentComment this.parentComment = parentComment
replyRequestGeneration++ replyRequestGeneration++
isLoadingReplies = false isLoadingReplies = false
_replyStateLiveData.value = CreatorChannelCommunityReplyUiState.Content( _replyStateLiveData.value = CreatorChannelCommunityReplyUiState.Content(
parentComment = formattedParentComment, parentComment = parentComment,
replies = emptyList(), replies = emptyList(),
replyPage = FIRST_PAGE, replyPage = FIRST_PAGE,
hasNextReply = false hasNextReply = false
) )
if (formattedParentComment.commentId > 0) { if (parentComment.commentId > 0) {
loadReplyPage(page = FIRST_PAGE, append = false) loadReplyPage(page = FIRST_PAGE, append = false)
} }
} }
@@ -486,7 +481,6 @@ class CreatorChannelCommunityReplyViewModel(
memberNickname = writerNickname, memberNickname = writerNickname,
memberProfileUrl = writerProfileImageUrl, memberProfileUrl = writerProfileImageUrl,
createdAtUtc = createdAtUtc, createdAtUtc = createdAtUtc,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
comment = content, comment = content,
likeCount = 0, likeCount = 0,
isLiked = false, isLiked = false,
@@ -7,6 +7,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityReplyBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityReplyBinding
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityReplyUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityReplyUiModel
@@ -51,7 +52,8 @@ class CreatorChannelCommunityReplyAdapter(
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
tvCreatorChannelCommunityReplyItemNickname.text = item.memberNickname tvCreatorChannelCommunityReplyItemNickname.text = item.memberNickname
tvCreatorChannelCommunityReplyItemTime.text = item.createdAtText tvCreatorChannelCommunityReplyItemTime.text =
formatUtcRelativeTimeText(tvCreatorChannelCommunityReplyItemTime.context, item.createdAtUtc)
tvCreatorChannelCommunityReplyItemBody.text = item.comment tvCreatorChannelCommunityReplyItemBody.text = item.comment
ivCreatorChannelCommunityReplyItemMore.isVisible = item.canShowMore ivCreatorChannelCommunityReplyItemMore.isVisible = item.canShowMore
ivCreatorChannelCommunityReplyItemMore.setOnClickListener { anchor -> ivCreatorChannelCommunityReplyItemMore.setOnClickListener { anchor ->
@@ -7,6 +7,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityCommentBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityCommentBinding
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
@@ -70,7 +71,8 @@ class CreatorChannelCommunityCommentAdapter(
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
tvCreatorChannelCommunityCommentNickname.text = item.memberNickname tvCreatorChannelCommunityCommentNickname.text = item.memberNickname
tvCreatorChannelCommunityCommentTime.text = item.createdAtText tvCreatorChannelCommunityCommentTime.text =
formatUtcRelativeTimeText(tvCreatorChannelCommunityCommentTime.context, item.createdAtUtc)
tvCreatorChannelCommunityCommentBody.text = item.comment tvCreatorChannelCommunityCommentBody.text = item.comment
ivCreatorChannelCommunityCommentMore.isVisible = item.canShowMore && onDeleteClick != null ivCreatorChannelCommunityCommentMore.isVisible = item.canShowMore && onDeleteClick != null
ivCreatorChannelCommunityCommentMore.setOnClickListener { anchor -> ivCreatorChannelCommunityCommentMore.setOnClickListener { anchor ->
@@ -1,20 +1,17 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community.model package kr.co.vividnext.sodalive.v2.creator.channel.community.model
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityPostResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityPostResponse
private const val GRID_PREVIEW_MAX_LENGTH = 24 private const val GRID_PREVIEW_MAX_LENGTH = 24
fun List<CreatorChannelCommunityPostResponse>.toCommunityPostUiModels( fun List<CreatorChannelCommunityPostResponse>.toCommunityPostUiModels(
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter,
isOwner: Boolean, isOwner: Boolean,
currentUserId: Long currentUserId: Long
): List<CreatorChannelCommunityPostUiModel> = map { ): List<CreatorChannelCommunityPostUiModel> = map {
it.toCommunityPostUiModel(relativeTimeTextFormatter, isOwner, currentUserId) it.toCommunityPostUiModel(isOwner, currentUserId)
} }
private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel( private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel(
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter,
isOwner: Boolean, isOwner: Boolean,
currentUserId: Long currentUserId: Long
): CreatorChannelCommunityPostUiModel { ): CreatorChannelCommunityPostUiModel {
@@ -26,7 +23,7 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel(
creatorId = creatorId, creatorId = creatorId,
creatorNickname = creatorNickname, creatorNickname = creatorNickname,
creatorProfileUrl = creatorProfileUrl, creatorProfileUrl = creatorProfileUrl,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc), createdAtUtc = createdAtUtc,
content = content, content = content,
imageUrl = visibleImageUrl, imageUrl = visibleImageUrl,
price = price, price = price,
@@ -29,7 +29,7 @@ data class CreatorChannelCommunityPostUiModel(
val creatorId: Long, val creatorId: Long,
val creatorNickname: String, val creatorNickname: String,
val creatorProfileUrl: String, val creatorProfileUrl: String,
val createdAtText: String, val createdAtUtc: String,
val content: String, val content: String,
val imageUrl: String?, val imageUrl: String?,
val price: Int, val price: Int,
@@ -15,6 +15,7 @@ import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityListBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
@@ -68,7 +69,8 @@ class CreatorChannelCommunityListAdapter(
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
tvCreatorChannelCommunityListNickname.text = item.creatorNickname tvCreatorChannelCommunityListNickname.text = item.creatorNickname
tvCreatorChannelCommunityListTime.text = item.createdAtText tvCreatorChannelCommunityListTime.text =
formatUtcRelativeTimeText(tvCreatorChannelCommunityListTime.context, item.createdAtUtc)
layoutCreatorChannelCommunityListNotice.isVisible = item.showNotice layoutCreatorChannelCommunityListNotice.isVisible = item.showNotice
tvCreatorChannelCommunityListBody.text = item.content tvCreatorChannelCommunityListBody.text = item.content
layoutCreatorChannelCommunityListReaction.isVisible = item.showReaction || item.showComment layoutCreatorChannelCommunityListReaction.isVisible = item.showReaction || item.showComment
@@ -194,7 +194,7 @@ private data class CreatorChannelDonationItemLayoutKey(
val nickname: String, val nickname: String,
val can: Int, val can: Int,
val message: String, val message: String,
val createdAtText: String val createdAtUtc: String
) )
private fun CreatorChannelDonationUiState.Content.toContentLayoutKey(): CreatorChannelDonationContentLayoutKey { private fun CreatorChannelDonationUiState.Content.toContentLayoutKey(): CreatorChannelDonationContentLayoutKey {
@@ -206,7 +206,7 @@ private fun CreatorChannelDonationUiState.Content.toContentLayoutKey(): CreatorC
nickname = donation.nickname, nickname = donation.nickname,
can = donation.can, can = donation.can,
message = donation.message, message = donation.message,
createdAtText = donation.createdAtText createdAtUtc = donation.createdAtUtc
) )
} }
) )
@@ -7,8 +7,6 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.CreatorChannelDonationTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.CreatorChannelDonationTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.CreatorChannelDonationRankingUiModel import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.CreatorChannelDonationRankingUiModel
@@ -17,15 +15,13 @@ import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.toDonationRank
import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.toDonationUiModels import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.toDonationUiModels
class CreatorChannelDonationViewModel( class CreatorChannelDonationViewModel(
private val repository: CreatorChannelRepository, private val repository: CreatorChannelRepository
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() { ) : BaseViewModel() {
private val _donationStateLiveData = MutableLiveData<CreatorChannelDonationUiState>() private val _donationStateLiveData = MutableLiveData<CreatorChannelDonationUiState>()
val donationStateLiveData: LiveData<CreatorChannelDonationUiState> val donationStateLiveData: LiveData<CreatorChannelDonationUiState>
get() = _donationStateLiveData get() = _donationStateLiveData
private val context = SodaLiveApplicationHolder.get()
private var creatorId: Long = 0L private var creatorId: Long = 0L
private var isOwner: Boolean = false private var isOwner: Boolean = false
private var requestGeneration: Int = 0 private var requestGeneration: Int = 0
@@ -222,7 +218,7 @@ class CreatorChannelDonationViewModel(
rankings.toDonationRankingUiModels() rankings.toDonationRankingUiModels()
private fun CreatorChannelDonationTabResponse.toDonationUiModels(): List<CreatorChannelDonationUiModel> = private fun CreatorChannelDonationTabResponse.toDonationUiModels(): List<CreatorChannelDonationUiModel> =
donations.toDonationUiModels(context, relativeTimeTextFormatter) donations.toDonationUiModels()
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}" private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
@@ -1,19 +1,15 @@
package kr.co.vividnext.sodalive.v2.creator.channel.donation.model package kr.co.vividnext.sodalive.v2.creator.channel.donation.model
import android.content.Context
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.CreatorChannelDonationResponse import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.CreatorChannelDonationResponse
import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.MemberDonationRankingResponse import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.MemberDonationRankingResponse
fun List<MemberDonationRankingResponse>.toDonationRankingUiModels(): List<CreatorChannelDonationRankingUiModel> = fun List<MemberDonationRankingResponse>.toDonationRankingUiModels(): List<CreatorChannelDonationRankingUiModel> =
mapIndexed { index, response -> response.toDonationRankingUiModel(rank = index + 1) } mapIndexed { index, response -> response.toDonationRankingUiModel(rank = index + 1) }
fun List<CreatorChannelDonationResponse>.toDonationUiModels( fun List<CreatorChannelDonationResponse>.toDonationUiModels(): List<CreatorChannelDonationUiModel> =
context: Context, map { it.toDonationUiModel() }
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
): List<CreatorChannelDonationUiModel> = map { it.toDonationUiModel(context, relativeTimeTextFormatter) }
private fun MemberDonationRankingResponse.toDonationRankingUiModel(rank: Int) = CreatorChannelDonationRankingUiModel( private fun MemberDonationRankingResponse.toDonationRankingUiModel(rank: Int) = CreatorChannelDonationRankingUiModel(
rank = rank, rank = rank,
@@ -23,15 +19,12 @@ private fun MemberDonationRankingResponse.toDonationRankingUiModel(rank: Int) =
donationCan = donationCan donationCan = donationCan
) )
private fun CreatorChannelDonationResponse.toDonationUiModel( private fun CreatorChannelDonationResponse.toDonationUiModel() = CreatorChannelDonationUiModel(
context: Context,
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) = CreatorChannelDonationUiModel(
nickname = nickname, nickname = nickname,
profileImageUrl = profileImageUrl, profileImageUrl = profileImageUrl,
can = can, can = can,
message = message.takeUnless { it.isBlank() } ?: context.getString(R.string.creator_channel_donation_fallback_message, can), message = message,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc), createdAtUtc = createdAtUtc,
headerColorResId = calculateDonationHeaderColorRes(can) headerColorResId = calculateDonationHeaderColorRes(can)
) )
@@ -15,6 +15,6 @@ data class CreatorChannelDonationUiModel(
val profileImageUrl: String, val profileImageUrl: String,
val can: Int, val can: Int,
val message: String, val message: String,
val createdAtText: String, val createdAtUtc: String,
@param:ColorRes val headerColorResId: Int @param:ColorRes val headerColorResId: Int
) )
@@ -8,6 +8,8 @@ import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationBinding
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationEmptyBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationEmptyBinding
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationRankingBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationRankingBinding
@@ -115,6 +117,7 @@ class CreatorChannelDonationAdapter(
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: CreatorChannelDonationUiModel) = with(binding) { fun bind(item: CreatorChannelDonationUiModel) = with(binding) {
val context = root.context
bindCreatorChannelDonationCard( bindCreatorChannelDonationCard(
headerView = layoutCreatorChannelDonationHeader, headerView = layoutCreatorChannelDonationHeader,
profileView = ivCreatorChannelDonationProfile, profileView = ivCreatorChannelDonationProfile,
@@ -125,9 +128,10 @@ class CreatorChannelDonationAdapter(
headerColorResId = item.headerColorResId, headerColorResId = item.headerColorResId,
profileImageUrl = item.profileImageUrl, profileImageUrl = item.profileImageUrl,
nickname = item.nickname, nickname = item.nickname,
createdAtText = item.createdAtText, createdAtText = formatUtcRelativeTimeText(context, item.createdAtUtc),
can = item.can, can = item.can,
message = item.message message = item.message.takeUnless { it.isBlank() }
?: context.getString(R.string.creator_channel_donation_fallback_message, item.can)
) )
} }
} }
@@ -9,15 +9,13 @@ import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.base.BaseViewModel import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkUiModel import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.toFanTalkUiModels import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.toFanTalkUiModels
class CreatorChannelFanTalkViewModel( class CreatorChannelFanTalkViewModel(
private val repository: CreatorChannelRepository, private val repository: CreatorChannelRepository
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() { ) : BaseViewModel() {
private val _fanTalkStateLiveData = MutableLiveData<CreatorChannelFanTalkUiState>() private val _fanTalkStateLiveData = MutableLiveData<CreatorChannelFanTalkUiState>()
@@ -198,7 +196,6 @@ class CreatorChannelFanTalkViewModel(
private fun CreatorChannelFanTalkTabResponse.toFanTalkUiModels(): List<CreatorChannelFanTalkUiModel> = private fun CreatorChannelFanTalkTabResponse.toFanTalkUiModels(): List<CreatorChannelFanTalkUiModel> =
fanTalks.toFanTalkUiModels( fanTalks.toFanTalkUiModels(
relativeTimeTextFormatter = relativeTimeTextFormatter,
isOwner = isOwner, isOwner = isOwner,
currentUserId = SharedPreferenceManager.userId currentUserId = SharedPreferenceManager.userId
) )
@@ -19,6 +19,7 @@ import androidx.core.widget.doAfterTextChanged
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelFantalkDetailBinding import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelFantalkDetailBinding
import kr.co.vividnext.sodalive.databinding.ViewCreatorChannelFantalkMorePopupBinding import kr.co.vividnext.sodalive.databinding.ViewCreatorChannelFantalkMorePopupBinding
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
@@ -107,7 +108,8 @@ class CreatorChannelFanTalkDetailActivity : BaseActivity<ActivityCreatorChannelF
private fun bindParentFanTalk(item: CreatorChannelFanTalkUiModel) = with(binding) { private fun bindParentFanTalk(item: CreatorChannelFanTalkUiModel) = with(binding) {
ivCreatorChannelFantalkDetailProfile.loadProfile(item.writerProfileImageUrl) ivCreatorChannelFantalkDetailProfile.loadProfile(item.writerProfileImageUrl)
tvCreatorChannelFantalkDetailNickname.text = item.writerNickname tvCreatorChannelFantalkDetailNickname.text = item.writerNickname
tvCreatorChannelFantalkDetailTime.text = item.createdAtText tvCreatorChannelFantalkDetailTime.text =
formatUtcRelativeTimeText(tvCreatorChannelFantalkDetailTime.context, item.createdAtUtc)
tvCreatorChannelFantalkDetailContent.text = item.content tvCreatorChannelFantalkDetailContent.text = item.content
ivCreatorChannelFantalkDetailMore.isVisible = false ivCreatorChannelFantalkDetailMore.isVisible = false
ivCreatorChannelFantalkDetailMore.setOnClickListener(null) ivCreatorChannelFantalkDetailMore.setOnClickListener(null)
@@ -123,7 +125,8 @@ class CreatorChannelFanTalkDetailActivity : BaseActivity<ActivityCreatorChannelF
ivCreatorChannelFantalkDetailReplyProfile.loadProfile(reply.writerProfileImageUrl) ivCreatorChannelFantalkDetailReplyProfile.loadProfile(reply.writerProfileImageUrl)
tvCreatorChannelFantalkDetailReplyNickname.text = reply.writerNickname tvCreatorChannelFantalkDetailReplyNickname.text = reply.writerNickname
tvCreatorChannelFantalkDetailReplyTime.text = reply.createdAtText tvCreatorChannelFantalkDetailReplyTime.text =
formatUtcRelativeTimeText(tvCreatorChannelFantalkDetailReplyTime.context, reply.createdAtUtc)
tvCreatorChannelFantalkDetailReplyContent.text = reply.content tvCreatorChannelFantalkDetailReplyContent.text = reply.content
ivCreatorChannelFantalkDetailReplyMore.isVisible = reply.fanTalkId > 0L ivCreatorChannelFantalkDetailReplyMore.isVisible = reply.fanTalkId > 0L
ivCreatorChannelFantalkDetailReplyMore.setOnClickListener( ivCreatorChannelFantalkDetailReplyMore.setOnClickListener(
@@ -15,7 +15,7 @@ data class CreatorChannelFanTalkDetailPayload(
val writerNickname: String, val writerNickname: String,
val writerProfileImageUrl: String, val writerProfileImageUrl: String,
val content: String, val content: String,
val createdAtText: String, val createdAtUtc: String,
val reply: CreatorChannelFanTalkDetailReplyPayload?, val reply: CreatorChannelFanTalkDetailReplyPayload?,
val showEdit: Boolean, val showEdit: Boolean,
val showDelete: Boolean val showDelete: Boolean
@@ -26,7 +26,7 @@ data class CreatorChannelFanTalkDetailPayload(
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = createdAtText, createdAtUtc = createdAtUtc,
reply = reply?.toUiModel(), reply = reply?.toUiModel(),
rightAction = CreatorChannelFanTalkRightAction.OwnerMore(showEdit = showEdit, showDelete = showDelete) rightAction = CreatorChannelFanTalkRightAction.OwnerMore(showEdit = showEdit, showDelete = showDelete)
) )
@@ -39,7 +39,7 @@ data class CreatorChannelFanTalkDetailReplyPayload(
val writerNickname: String, val writerNickname: String,
val writerProfileImageUrl: String, val writerProfileImageUrl: String,
val content: String, val content: String,
val createdAtText: String val createdAtUtc: String
) : Parcelable { ) : Parcelable {
fun toUiModel(): CreatorChannelFanTalkReplyUiModel = CreatorChannelFanTalkReplyUiModel( fun toUiModel(): CreatorChannelFanTalkReplyUiModel = CreatorChannelFanTalkReplyUiModel(
fanTalkId = fanTalkId, fanTalkId = fanTalkId,
@@ -47,7 +47,7 @@ data class CreatorChannelFanTalkDetailReplyPayload(
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = createdAtText createdAtUtc = createdAtUtc
) )
} }
@@ -59,7 +59,7 @@ fun CreatorChannelFanTalkUiModel.toFanTalkDetailPayload(): CreatorChannelFanTalk
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = createdAtText, createdAtUtc = createdAtUtc,
reply = reply?.toFanTalkDetailReplyPayload(), reply = reply?.toFanTalkDetailReplyPayload(),
showEdit = ownerMore?.showEdit == true, showEdit = ownerMore?.showEdit == true,
showDelete = ownerMore?.showDelete == true showDelete = ownerMore?.showDelete == true
@@ -73,7 +73,7 @@ fun CreatorChannelFanTalkReplyUiModel.toFanTalkDetailReplyPayload(): CreatorChan
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = createdAtText createdAtUtc = createdAtUtc
) )
} }
@@ -11,15 +11,13 @@ import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.toFanTalkReplyUiModel import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.toFanTalkReplyUiModel
class CreatorChannelFanTalkDetailViewModel( class CreatorChannelFanTalkDetailViewModel(
private val repository: CreatorChannelRepository, private val repository: CreatorChannelRepository
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() { ) : BaseViewModel() {
private val _detailStateLiveData = MutableLiveData<CreatorChannelFanTalkDetailUiState>() private val _detailStateLiveData = MutableLiveData<CreatorChannelFanTalkDetailUiState>()
@@ -191,7 +189,7 @@ class CreatorChannelFanTalkDetailViewModel(
val current = currentContent() ?: return val current = currentContent() ?: return
if (response.success) { if (response.success) {
_detailStateLiveData.value = current.copy( _detailStateLiveData.value = current.copy(
reply = response.data?.toFanTalkReplyUiModel(relativeTimeTextFormatter) ?: current.reply, reply = response.data?.toFanTalkReplyUiModel() ?: current.reply,
replyInput = "", replyInput = "",
isSubmitting = false, isSubmitting = false,
editingTarget = null editingTarget = null
@@ -1,19 +1,16 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkResponse
fun List<CreatorChannelFanTalkResponse>.toFanTalkUiModels( fun List<CreatorChannelFanTalkResponse>.toFanTalkUiModels(
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter,
isOwner: Boolean, isOwner: Boolean,
currentUserId: Long currentUserId: Long
): List<CreatorChannelFanTalkUiModel> = map { ): List<CreatorChannelFanTalkUiModel> = map {
it.toFanTalkUiModel(relativeTimeTextFormatter, isOwner, currentUserId) it.toFanTalkUiModel(isOwner, currentUserId)
} }
private fun CreatorChannelFanTalkResponse.toFanTalkUiModel( private fun CreatorChannelFanTalkResponse.toFanTalkUiModel(
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter,
isOwner: Boolean, isOwner: Boolean,
currentUserId: Long currentUserId: Long
) = CreatorChannelFanTalkUiModel( ) = CreatorChannelFanTalkUiModel(
@@ -22,31 +19,27 @@ private fun CreatorChannelFanTalkResponse.toFanTalkUiModel(
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc), createdAtUtc = createdAtUtc,
reply = creatorReplies.orEmpty().firstOrNull()?.toFanTalkReplyUiModel(relativeTimeTextFormatter), reply = creatorReplies.orEmpty().firstOrNull()?.toFanTalkReplyUiModel(),
rightAction = toRightAction(isOwner = isOwner, currentUserId = currentUserId) rightAction = toRightAction(isOwner = isOwner, currentUserId = currentUserId)
) )
fun CreatorChannelFanTalkReplyResponse.toFanTalkReplyUiModel( fun CreatorChannelFanTalkReplyResponse.toFanTalkReplyUiModel() = CreatorChannelFanTalkReplyUiModel(
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) = CreatorChannelFanTalkReplyUiModel(
fanTalkId = fanTalkId, fanTalkId = fanTalkId,
writerId = writerId, writerId = writerId,
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc) createdAtUtc = createdAtUtc
) )
fun CreatorChannelFanTalkResponse.toFanTalkReplyUiModel( fun CreatorChannelFanTalkResponse.toFanTalkReplyUiModel() = CreatorChannelFanTalkReplyUiModel(
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) = CreatorChannelFanTalkReplyUiModel(
fanTalkId = fanTalkId, fanTalkId = fanTalkId,
writerId = writerId, writerId = writerId,
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc) createdAtUtc = createdAtUtc
) )
private fun CreatorChannelFanTalkResponse.toRightAction( private fun CreatorChannelFanTalkResponse.toRightAction(
@@ -6,7 +6,7 @@ data class CreatorChannelFanTalkUiModel(
val writerNickname: String, val writerNickname: String,
val writerProfileImageUrl: String, val writerProfileImageUrl: String,
val content: String, val content: String,
val createdAtText: String, val createdAtUtc: String,
val reply: CreatorChannelFanTalkReplyUiModel?, val reply: CreatorChannelFanTalkReplyUiModel?,
val rightAction: CreatorChannelFanTalkRightAction val rightAction: CreatorChannelFanTalkRightAction
) )
@@ -17,7 +17,7 @@ data class CreatorChannelFanTalkReplyUiModel(
val writerNickname: String, val writerNickname: String,
val writerProfileImageUrl: String, val writerProfileImageUrl: String,
val content: String, val content: String,
val createdAtText: String val createdAtUtc: String
) )
sealed interface CreatorChannelFanTalkRightAction { sealed interface CreatorChannelFanTalkRightAction {
@@ -7,6 +7,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelFantalkBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelFantalkBinding
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkReplyUiModel import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkReplyUiModel
@@ -51,7 +52,8 @@ class CreatorChannelFanTalkAdapter(
fun bind(item: CreatorChannelFanTalkUiModel) = with(binding) { fun bind(item: CreatorChannelFanTalkUiModel) = with(binding) {
ivCreatorChannelFantalkProfile.loadProfile(item.writerProfileImageUrl) ivCreatorChannelFantalkProfile.loadProfile(item.writerProfileImageUrl)
tvCreatorChannelFantalkNickname.text = item.writerNickname tvCreatorChannelFantalkNickname.text = item.writerNickname
tvCreatorChannelFantalkTime.text = item.createdAtText tvCreatorChannelFantalkTime.text =
formatUtcRelativeTimeText(tvCreatorChannelFantalkTime.context, item.createdAtUtc)
tvCreatorChannelFantalkContent.text = item.content tvCreatorChannelFantalkContent.text = item.content
root.setOnClickListener { onItemClick(item) } root.setOnClickListener { onItemClick(item) }
tvCreatorChannelFantalkContent.setOnClickListener { onItemClick(item) } tvCreatorChannelFantalkContent.setOnClickListener { onItemClick(item) }
@@ -63,6 +65,7 @@ class CreatorChannelFanTalkAdapter(
when (item.rightAction) { when (item.rightAction) {
CreatorChannelFanTalkRightAction.Report -> { CreatorChannelFanTalkRightAction.Report -> {
tvCreatorChannelFantalkReport.isVisible = true tvCreatorChannelFantalkReport.isVisible = true
tvCreatorChannelFantalkReport.text = root.context.getString(R.string.creator_channel_fantalk_report)
tvCreatorChannelFantalkReport.setOnClickListener { onReportClick(item) } tvCreatorChannelFantalkReport.setOnClickListener { onReportClick(item) }
ivCreatorChannelFantalkMore.isVisible = false ivCreatorChannelFantalkMore.isVisible = false
ivCreatorChannelFantalkMore.setOnClickListener(null) ivCreatorChannelFantalkMore.setOnClickListener(null)
@@ -84,7 +87,8 @@ class CreatorChannelFanTalkAdapter(
ivCreatorChannelFantalkReplyProfile.loadProfile(reply.writerProfileImageUrl) ivCreatorChannelFantalkReplyProfile.loadProfile(reply.writerProfileImageUrl)
tvCreatorChannelFantalkReplyNickname.text = reply.writerNickname tvCreatorChannelFantalkReplyNickname.text = reply.writerNickname
tvCreatorChannelFantalkReplyTime.text = reply.createdAtText tvCreatorChannelFantalkReplyTime.text =
formatUtcRelativeTimeText(tvCreatorChannelFantalkReplyTime.context, reply.createdAtUtc)
tvCreatorChannelFantalkReplyContent.text = reply.content tvCreatorChannelFantalkReplyContent.text = reply.content
} }
@@ -33,7 +33,7 @@ internal fun bindCreatorChannelDonationCard(
} }
nicknameView.text = nickname nicknameView.text = nickname
createdAtView.text = createdAtText createdAtView.text = createdAtText
canView.text = context.getString(R.string.creator_channel_donation_can_format, can.moneyFormat()) canView.text = can.moneyFormat()
messageView.text = message messageView.text = message
if (headerColorResId == R.color.red_400) { if (headerColorResId == R.color.red_400) {
nicknameView.setTextColor(context.getColor(R.color.white)) nicknameView.setTextColor(context.getColor(R.color.white))
@@ -1,13 +1,13 @@
package kr.co.vividnext.sodalive.v2.main package kr.co.vividnext.sodalive.v2.main
import android.Manifest
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Build import android.os.Build
@@ -38,6 +38,8 @@ import com.google.firebase.messaging.FirebaseMessaging
import com.gun0912.tedpermission.PermissionListener import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission import com.gun0912.tedpermission.normal.TedPermission
import com.orhanobut.logger.Logger import com.orhanobut.logger.Logger
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
@@ -53,6 +55,7 @@ import kr.co.vividnext.sodalive.databinding.ActivityMainV2Binding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.LiveViewModel import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity
import kr.co.vividnext.sodalive.main.EventPopupDialogFragment import kr.co.vividnext.sodalive.main.EventPopupDialogFragment
import kr.co.vividnext.sodalive.message.MessageActivity import kr.co.vividnext.sodalive.message.MessageActivity
@@ -79,9 +82,6 @@ import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomFilter
import kr.co.vividnext.sodalive.v2.main.content.ContentMainFragment import kr.co.vividnext.sodalive.v2.main.content.ContentMainFragment
import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType
import kr.co.vividnext.sodalive.v2.main.home.HomeMainFragment import kr.co.vividnext.sodalive.v2.main.home.HomeMainFragment
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import java.util.Locale import java.util.Locale
import kotlin.math.max import kotlin.math.max
@@ -108,6 +108,18 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
private var mediaControllerFuture: ListenableFuture<MediaController>? = null private var mediaControllerFuture: ListenableFuture<MediaController>? = null
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val showMiniPlayerRunnable = Runnable { initAndVisibleMiniPlayer() } private val showMiniPlayerRunnable = Runnable { initAndVisibleMiniPlayer() }
private val deferredRouteRunnable = Runnable {
if (isFinishing || isDestroyed) return@Runnable
try {
if (!handleAudioNotificationRoute(intent) && isV2AccessAllowed(AccessRequirement.Login)) {
executeDeeplink(intent)
}
} finally {
isDeferredRouteLoading = false
updateRouteLoadingDialog()
}
}
private val audioContentReceiver = AudioContentReceiver() private val audioContentReceiver = AudioContentReceiver()
private var playerStateJob: Job? = null private var playerStateJob: Job? = null
private var isDeferredRouteLoading = false private var isDeferredRouteLoading = false
@@ -145,13 +157,7 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
trackAppLaunchIfNeeded() trackAppLaunchIfNeeded()
pushTokenUpdate() pushTokenUpdate()
handler.postDelayed({ handler.postDelayed(deferredRouteRunnable, 1000)
if (!handleAudioNotificationRoute(intent) && isV2AccessAllowed(AccessRequirement.Login)) {
executeDeeplink(intent)
}
isDeferredRouteLoading = false
updateRouteLoadingDialog()
}, 1000)
if (isV2AccessAllowed(AccessRequirement.Login)) { if (isV2AccessAllowed(AccessRequirement.Login)) {
updatePidAndGaid() updatePidAndGaid()
@@ -178,15 +184,27 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
setIntent(intent) setIntent(intent)
handler.removeCallbacks(deferredRouteRunnable)
isDeferredRouteLoading = false
updateRouteLoadingDialog()
if (intent.hasExtra(EXTRA_CHAT_FILTER)) { if (intent.hasExtra(EXTRA_CHAT_FILTER)) {
openChatWithInitialFilter() openChatWithInitialFilter()
return return
} }
if (!handleAudioNotificationRoute(intent) && isV2AccessAllowed(AccessRequirement.Login)) { if (handleAudioNotificationRoute(intent) || !isV2AccessAllowed(AccessRequirement.Login)) {
executeDeeplink(intent) return
} }
if (LiveRoomActivity.isForeground && isLiveRoomDeepLink(intent)) {
isDeferredRouteLoading = true
updateRouteLoadingDialog()
handler.postDelayed(deferredRouteRunnable, 1000)
return
}
executeDeeplink(intent)
} }
@SuppressLint("UnspecifiedRegisterReceiverFlag") @SuppressLint("UnspecifiedRegisterReceiverFlag")
@@ -213,6 +231,9 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
} }
override fun onDestroy() { override fun onDestroy() {
handler.removeCallbacks(deferredRouteRunnable)
isDeferredRouteLoading = false
routeLoadingDialog.dismiss()
deInitMiniPlayer() deInitMiniPlayer()
playerStateJob?.cancel() playerStateJob?.cancel()
super.onDestroy() super.onDestroy()
@@ -315,15 +336,18 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
viewModel.clickTab(MainV2Tab.HOME) viewModel.clickTab(MainV2Tab.HOME)
true true
} }
R.id.menu_main_v2_content -> { R.id.menu_main_v2_content -> {
viewModel.clickTab(MainV2Tab.CONTENT) viewModel.clickTab(MainV2Tab.CONTENT)
true true
} }
R.id.menu_main_v2_chat -> selectChatTabWithLoginGuard() R.id.menu_main_v2_chat -> selectChatTabWithLoginGuard()
R.id.menu_main_v2_my -> { R.id.menu_main_v2_my -> {
viewModel.clickTab(MainV2Tab.MY) viewModel.clickTab(MainV2Tab.MY)
true true
} }
else -> false else -> false
} }
} }
@@ -757,19 +781,53 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
} }
private fun executeDeeplink(intent: Intent) { private fun executeDeeplink(intent: Intent) {
val bundle = intent.getBundleExtra(Constants.EXTRA_DATA) ?: return val routeBundle = resolveDeepLinkBundle(intent) ?: return
if (executeBundleRoute(routeBundle)) {
clearDeferredDeepLink()
}
}
private fun resolveDeepLinkBundle(intent: Intent): Bundle? {
val bundle = intent.getBundleExtra(Constants.EXTRA_DATA) ?: return null
val deepLinkUrl = bundle.getString("deep_link") val deepLinkUrl = bundle.getString("deep_link")
val routeBundle = if (!deepLinkUrl.isNullOrBlank()) { return if (!deepLinkUrl.isNullOrBlank()) {
Bundle(bundle).apply { Bundle(bundle).apply {
buildBundleFromDeepLinkUrl(deepLinkUrl)?.let { putAll(it) } buildBundleFromDeepLinkUrl(deepLinkUrl)?.let { putAll(it) }
} }
} else { } else {
bundle bundle
} }
}
if (executeBundleRoute(routeBundle)) { private fun isLiveRoomDeepLink(intent: Intent): Boolean {
clearDeferredDeepLink() val bundle = resolveDeepLinkBundle(intent) ?: return false
if (isDmChatDeepLink(bundle)) return false
val roomId = bundle.getString("room_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_ROOM_ID).takeIf { it > 0 }
if (roomId != null && roomId > 0) return true
if (hasDirectNonLiveRoute(bundle)) return false
val deepLinkValueId = bundle.getString("deep_link_sub5")?.toLongOrNull()
return bundle.getString("deep_link_value")?.lowercase(Locale.ROOT) == "live" &&
deepLinkValueId != null && deepLinkValueId > 0
}
private fun hasDirectNonLiveRoute(bundle: Bundle): Boolean {
fun hasPositiveId(stringKey: String, longKey: String): Boolean {
return (
bundle.getString(stringKey)?.toLongOrNull()
?: bundle.getLong(longKey).takeIf { it > 0 }
)?.let { it > 0 } == true
} }
return hasPositiveId(Constants.EXTRA_COMMUNITY_POST_ID, Constants.EXTRA_COMMUNITY_POST_ID) ||
hasPositiveId("channel_id", Constants.EXTRA_USER_ID) ||
hasPositiveId("content_id", Constants.EXTRA_AUDIO_CONTENT_ID) ||
hasPositiveId("message_id", Constants.EXTRA_MESSAGE_ID) ||
hasPositiveId(Constants.EXTRA_COMMUNITY_CREATOR_ID, Constants.EXTRA_COMMUNITY_CREATOR_ID) ||
hasPositiveId("audition_id", Constants.EXTRA_AUDITION_ID)
} }
private fun buildBundleFromDeepLinkUrl(deepLinkUrl: String): Bundle? { private fun buildBundleFromDeepLinkUrl(deepLinkUrl: String): Bundle? {
@@ -3,8 +3,6 @@ package kr.co.vividnext.sodalive.v2.main.chat
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -12,9 +10,10 @@ import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentV2MainChatBinding import kr.co.vividnext.sodalive.databinding.FragmentV2MainChatBinding
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
import kr.co.vividnext.sodalive.search.SearchActivity import kr.co.vividnext.sodalive.settings.notification.MemberRole
import kr.co.vividnext.sodalive.v2.access.AccessRequirement import kr.co.vividnext.sodalive.v2.access.AccessRequirement
import kr.co.vividnext.sodalive.v2.access.ensureV2Access import kr.co.vividnext.sodalive.v2.access.ensureV2Access
import kr.co.vividnext.sodalive.v2.chat.action.ChatActionCommand import kr.co.vividnext.sodalive.v2.chat.action.ChatActionCommand
@@ -24,6 +23,7 @@ import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiItem
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiState import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiState
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomType import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomType
import kr.co.vividnext.sodalive.v2.main.chat.model.emptyMessageResId import kr.co.vividnext.sodalive.v2.main.chat.model.emptyMessageResId
import kr.co.vividnext.sodalive.v2.main.chat.recipient.DmRecipientPickerActivity
import kr.co.vividnext.sodalive.v2.main.chat.ui.ChatRoomListAdapter import kr.co.vividnext.sodalive.v2.main.chat.ui.ChatRoomListAdapter
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -51,12 +51,19 @@ class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
private var isChatPullRefreshing = false private var isChatPullRefreshing = false
private var refreshingChatFilter: ChatRoomFilter? = null private var refreshingChatFilter: ChatRoomFilter? = null
private var lastRenderedChatContentState: ChatRoomListUiState.Content? = null private var lastRenderedChatContentState: ChatRoomListUiState.Content? = null
private var shouldRefreshChatRoomsOnResume = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
shouldRefreshChatRoomsOnResume = false
loadingDialog = LoadingDialog(requireActivity(), layoutInflater) loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupTitleBar() setupTitleBar()
val isCreator = SharedPreferenceManager.role == MemberRole.CREATOR.name
binding.btnNewDm.isVisible = isCreator
binding.btnNewDm.setOnClickListener {
if (isCreator) openWithLoginGuard(DmRecipientPickerActivity::class.java)
}
val initialFilter = getInitialFilter() ?: ChatRoomFilter.ALL val initialFilter = getInitialFilter() ?: ChatRoomFilter.ALL
selectedFilter = initialFilter selectedFilter = initialFilter
setupFilterTabs(initialFilter) setupFilterTabs(initialFilter)
@@ -67,6 +74,17 @@ class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
viewModel.loadFirstPage(initialFilter) viewModel.loadFirstPage(initialFilter)
} }
override fun onResume() {
super.onResume()
if (!shouldRefreshChatRoomsOnResume) {
shouldRefreshChatRoomsOnResume = true
return
}
cancelChatPullRefresh()
bindChatRooms(emptyList(), showEmpty = false)
viewModel.loadFirstPage(selectedFilter)
}
fun selectFilter(filter: ChatRoomFilter) { fun selectFilter(filter: ChatRoomFilter) {
if (refreshingChatFilter != null && refreshingChatFilter != filter) { if (refreshingChatFilter != null && refreshingChatFilter != filter) {
cancelChatPullRefresh() cancelChatPullRefresh()
@@ -82,20 +100,6 @@ class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
binding.viewChatTitleBar.ivTitleBarMenu.setOnClickListener { binding.viewChatTitleBar.ivTitleBarMenu.setOnClickListener {
openWithLoginGuard(CanChargeActivity::class.java) openWithLoginGuard(CanChargeActivity::class.java)
} }
val searchAction = ImageView(requireContext()).apply {
setImageResource(R.drawable.ic_bar_search)
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
marginStart = resources.getDimensionPixelSize(R.dimen.spacing_14)
}
}
searchAction.setOnClickListener {
openWithLoginGuard(SearchActivity::class.java)
}
binding.viewChatTitleBar.llTitleBarActions.addView(searchAction)
} }
private fun openWithLoginGuard(activityClass: Class<*>) { private fun openWithLoginGuard(activityClass: Class<*>) {
@@ -174,6 +178,7 @@ class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
binding.rvChatRooms.scrollToPosition(0) binding.rvChatRooms.scrollToPosition(0)
} }
} }
ChatRoomListUiState.Empty -> { ChatRoomListUiState.Empty -> {
lastRenderedChatContentState = null lastRenderedChatContentState = null
bindChatRooms(emptyList(), showEmpty = true) bindChatRooms(emptyList(), showEmpty = true)
@@ -182,6 +187,7 @@ class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
refreshingChatFilter = null refreshingChatFilter = null
} }
} }
is ChatRoomListUiState.Error -> { is ChatRoomListUiState.Error -> {
lastRenderedChatContentState = null lastRenderedChatContentState = null
if (!isChatPullRefreshing || !isRefreshForCurrentFilter) { if (!isChatPullRefreshing || !isRefreshForCurrentFilter) {
@@ -192,6 +198,7 @@ class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
refreshingChatFilter = null refreshingChatFilter = null
} }
} }
ChatRoomListUiState.Loading -> { ChatRoomListUiState.Loading -> {
if (!isChatPullRefreshing) { if (!isChatPullRefreshing) {
binding.tvChatEmptyMessage.isVisible = false binding.tvChatEmptyMessage.isVisible = false
@@ -5,7 +5,8 @@ import com.google.gson.annotations.SerializedName
@Keep @Keep
data class CreateDmChatRoomRequest( data class CreateDmChatRoomRequest(
@SerializedName("creatorId") val creatorId: Long @SerializedName("recipientId") val recipientId: Long? = null,
@SerializedName("creatorId") val creatorId: Long? = null
) )
@Keep @Keep
@@ -18,6 +18,14 @@ class DmChatRepository(
request = CreateDmChatRoomRequest(creatorId = creatorId) request = CreateDmChatRoomRequest(creatorId = creatorId)
) )
fun createOrGetRoomByRecipientId(
token: String,
recipientId: Long
): Single<ApiResponse<CreateDmChatRoomResponse>> = api.createDmChatRoom(
authHeader = bearer(token),
request = CreateDmChatRoomRequest(recipientId = recipientId)
)
fun openRoom( fun openRoom(
token: String, token: String,
roomId: Long, roomId: Long,
@@ -0,0 +1,36 @@
package kr.co.vividnext.sodalive.v2.main.chat.recipient
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.databinding.ItemDmRecipientBinding
import kr.co.vividnext.sodalive.v2.main.home.ui.loadHomeCreatorProfileImage
class DmRecipientAdapter(
private val onClick: (DmRecipientUiModel) -> Unit
) : RecyclerView.Adapter<DmRecipientAdapter.RecipientViewHolder>() {
private var items: List<DmRecipientUiModel> = emptyList()
fun submitItems(items: List<DmRecipientUiModel>) {
this.items = items
notifyDataSetChanged()
}
override fun getItemCount(): Int = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipientViewHolder =
RecipientViewHolder(ItemDmRecipientBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: RecipientViewHolder, position: Int) {
holder.bind(items[position])
}
inner class RecipientViewHolder(private val binding: ItemDmRecipientBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: DmRecipientUiModel) {
binding.ivProfile.loadHomeCreatorProfileImage(item.profileImageUrl)
binding.tvNickname.text = item.nickname
binding.root.setOnClickListener { onClick(item) }
}
}
}
@@ -0,0 +1,113 @@
package kr.co.vividnext.sodalive.v2.main.chat.recipient
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.LinearLayoutManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityDmRecipientPickerBinding
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.settings.notification.MemberRole
import kr.co.vividnext.sodalive.user.UserRepository
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivity
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRepository
import org.koin.android.ext.android.inject
class DmRecipientPickerActivity : BaseActivity<ActivityDmRecipientPickerBinding>(
ActivityDmRecipientPickerBinding::inflate
) {
private val explorerRepository: ExplorerRepository by inject()
private val userRepository: UserRepository by inject()
private val dmRepository: DmChatRepository by inject()
private val viewModel: DmRecipientPickerViewModel by viewModels {
dmRecipientPickerFactory(explorerRepository, userRepository)
}
private val adapter = DmRecipientAdapter(::confirmRecipient)
private val loadingDialog by lazy { LoadingDialog(this, layoutInflater) }
private var isOpeningRoom = false
override fun setupView() {
if (SharedPreferenceManager.role != MemberRole.CREATOR.name) {
finish()
return
}
binding.btnBack.setOnClickListener { finish() }
binding.rvRecipients.layoutManager = LinearLayoutManager(this)
binding.rvRecipients.adapter = adapter
binding.etSearchNickname.doAfterTextChanged {
viewModel.onSearchQueryChanged(it?.toString().orEmpty())
}
viewModel.recipientStateLiveData.observe(this) { state ->
adapter.submitItems(state.recipients)
binding.tvFollowers.isVisible = state.mode == DmRecipientPickerMode.FOLLOWERS && state.recipients.isNotEmpty()
binding.tvSearchResultCount.isVisible = state.mode == DmRecipientPickerMode.SEARCH
val searchResultCount = state.searchResultCount.toString()
val searchResultText = getString(
R.string.dm_recipient_search_result_count,
state.searchResultCount
)
binding.tvSearchResultCount.text = SpannableString(searchResultText).apply {
val start = searchResultText.lastIndexOf(searchResultCount)
setSpan(
ForegroundColorSpan(ContextCompat.getColor(this@DmRecipientPickerActivity, R.color.gray_500)),
start,
start + searchResultCount.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
binding.tvSearchEmpty.isVisible = state.isSearchEmpty
binding.rvRecipients.isVisible = !state.isSearchEmpty
if (state.isError) showToast(getString(R.string.common_error_request))
}
viewModel.enter()
}
private fun confirmRecipient(recipient: DmRecipientUiModel) {
if (isOpeningRoom) return
V2ModalDialog(
activity = this,
layoutInflater = layoutInflater,
title = getString(R.string.dm_recipient_confirm_title),
desc = getString(R.string.dm_recipient_confirm_message, recipient.nickname),
cancelButtonTitle = getString(R.string.cancel),
confirmButtonTitle = getString(R.string.dm_recipient_send),
confirmButtonClick = { openRoom(recipient.id) }
).show(screenWidth)
}
private fun openRoom(recipientId: Long) {
if (isOpeningRoom) return
isOpeningRoom = true
loadingDialog.show(screenWidth)
compositeDisposable.add(
dmRepository.createOrGetRoomByRecipientId(SharedPreferenceManager.token, recipientId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally {
isOpeningRoom = false
loadingDialog.dismiss()
}
.subscribe({ response ->
val data = response.data
if (response.success && data != null) {
startActivity(DmChatRoomActivity.newIntentByRoomId(this, data.roomId))
finish()
} else {
showToast(getString(R.string.common_error_request))
}
}, {
showToast(getString(R.string.common_error_request))
})
)
}
}
@@ -0,0 +1,40 @@
package kr.co.vividnext.sodalive.v2.main.chat.recipient
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.user.UserRepository
internal fun dmRecipientPickerFactory(
explorerRepository: ExplorerRepository,
userRepository: UserRepository
) = viewModelFactory {
initializer {
DmRecipientPickerViewModel(
loadFollowers = {
explorerRepository.getFollowerList(
userId = SharedPreferenceManager.userId,
page = 1,
size = 20,
token = "Bearer ${SharedPreferenceManager.token}"
).map { response ->
val data = response.data
check(response.success && data != null)
data.items.map { DmRecipientUiModel(it.userId, it.nickname, it.profileImage) }
}.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
},
searchUsers = { nickname ->
userRepository.searchUser(nickname, "Bearer ${SharedPreferenceManager.token}")
.map { response ->
val data = response.data
check(response.success && data != null)
data.map { DmRecipientUiModel(it.id, it.nickname, it.profileImageUrl) }
}.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
},
debounceScheduler = AndroidSchedulers.mainThread()
)
}
}
@@ -0,0 +1,97 @@
package kr.co.vividnext.sodalive.v2.main.chat.recipient
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.reactivex.rxjava3.core.Scheduler
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.disposables.SerialDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel
import java.util.concurrent.TimeUnit
data class DmRecipientUiModel(
val id: Long,
val nickname: String,
val profileImageUrl: String
)
enum class DmRecipientPickerMode {
FOLLOWERS,
SEARCH
}
data class DmRecipientPickerUiState(
val mode: DmRecipientPickerMode = DmRecipientPickerMode.FOLLOWERS,
val recipients: List<DmRecipientUiModel> = emptyList(),
val searchResultCount: Int = 0,
val isSearchEmpty: Boolean = false,
val isError: Boolean = false
)
class DmRecipientPickerViewModel(
private val loadFollowers: () -> Single<List<DmRecipientUiModel>>,
private val searchUsers: (String) -> Single<List<DmRecipientUiModel>>,
private val debounceScheduler: Scheduler = Schedulers.computation()
) : BaseViewModel() {
private val _recipientStateLiveData = MutableLiveData(DmRecipientPickerUiState())
val recipientStateLiveData: LiveData<DmRecipientPickerUiState> get() = _recipientStateLiveData
private var searchDisposable: Disposable? = null
private val requestDisposable = SerialDisposable().also(compositeDisposable::add)
fun enter() {
requestDisposable.set(null)
requestDisposable.set(
loadFollowers()
.subscribe({ recipients ->
_recipientStateLiveData.value = DmRecipientPickerUiState(
mode = DmRecipientPickerMode.FOLLOWERS,
recipients = recipients
)
}, {
_recipientStateLiveData.value = DmRecipientPickerUiState(isError = true)
})
)
}
fun onSearchQueryChanged(query: String) {
val keyword = query.trim()
searchDisposable?.dispose()
requestDisposable.set(null)
if (keyword.length < MIN_SEARCH_QUERY_LENGTH) {
enter()
return
}
val scheduledSearch = debounceScheduler.scheduleDirect(
{
requestDisposable.set(
searchUsers(keyword).subscribe({ recipients ->
_recipientStateLiveData.value = DmRecipientPickerUiState(
mode = DmRecipientPickerMode.SEARCH,
recipients = recipients,
searchResultCount = recipients.size,
isSearchEmpty = recipients.isEmpty()
)
}, {
_recipientStateLiveData.value = DmRecipientPickerUiState(
mode = DmRecipientPickerMode.SEARCH,
isError = true
)
})
)
},
SEARCH_DEBOUNCE_MILLIS,
TimeUnit.MILLISECONDS
)
searchDisposable = scheduledSearch
compositeDisposable.add(scheduledSearch)
}
private companion object {
const val MIN_SEARCH_QUERY_LENGTH = 2
const val SEARCH_DEBOUNCE_MILLIS = 500L
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

@@ -27,7 +27,7 @@
android:textSize="18.3sp" android:textSize="18.3sp"
app:drawableStartCompat="@drawable/ic_back" app:drawableStartCompat="@drawable/ic_back"
tools:ignore="RelativeOverlap" tools:ignore="RelativeOverlap"
tools:text="보이스온" /> tools:text="소다라이브" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -226,6 +226,14 @@
tools:src="@drawable/ic_bar_bell" tools:src="@drawable/ic_bar_bell"
tools:visibility="visible" /> tools:visibility="visible" />
<ImageView
android:id="@+id/iv_share"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/screen_user_profile_share_channel"
android:src="@drawable/ic_audio_content_share"
android:visibility="gone" />
<ImageView <ImageView
android:id="@+id/iv_more" android:id="@+id/iv_more"
android:layout_width="24dp" android:layout_width="24dp"
@@ -53,43 +53,53 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<ImageView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/iv_creator_channel_community_detail_profile" android:id="@+id/layout_creator_channel_community_detail_creator"
android:layout_width="42dp"
android:layout_height="42dp"
android:contentDescription="@string/a11y_feed_profile_image"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/bg_placeholder" />
<TextView
android:id="@+id/tv_creator_channel_community_detail_nickname"
style="@style/Typography.Body5"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_8"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/white"
app:layout_constraintEnd_toStartOf="@id/btn_creator_channel_community_detail_more" app:layout_constraintEnd_toStartOf="@id/btn_creator_channel_community_detail_more"
app:layout_constraintStart_toEndOf="@id/iv_creator_channel_community_detail_profile" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile" app:layout_constraintTop_toTopOf="parent">
tools:text="크리에이터" />
<TextView <ImageView
android:id="@+id/tv_creator_channel_community_detail_time" android:id="@+id/iv_creator_channel_community_detail_profile"
style="@style/Typography.Body6" android:layout_width="42dp"
android:layout_width="0dp" android:layout_height="42dp"
android:layout_height="wrap_content" android:contentDescription="@string/a11y_feed_profile_image"
android:layout_marginTop="2dp" android:scaleType="centerCrop"
android:includeFontPadding="false" app:layout_constraintStart_toStartOf="parent"
android:textColor="@color/gray_500" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="@id/tv_creator_channel_community_detail_nickname" tools:src="@drawable/bg_placeholder" />
app:layout_constraintStart_toStartOf="@id/tv_creator_channel_community_detail_nickname"
app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_community_detail_nickname" <TextView
tools:text="2분 전" /> android:id="@+id/tv_creator_channel_community_detail_nickname"
style="@style/Typography.Body5"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_8"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_creator_channel_community_detail_profile"
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile"
tools:text="크리에이터" />
<TextView
android:id="@+id/tv_creator_channel_community_detail_time"
style="@style/Typography.Body6"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:includeFontPadding="false"
android:textColor="@color/gray_500"
app:layout_constraintEnd_toEndOf="@id/tv_creator_channel_community_detail_nickname"
app:layout_constraintStart_toStartOf="@id/tv_creator_channel_community_detail_nickname"
app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_community_detail_nickname"
tools:text="2분 전" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageButton <ImageButton
android:id="@+id/btn_creator_channel_community_detail_more" android:id="@+id/btn_creator_channel_community_detail_more"
@@ -88,7 +88,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:includeFontPadding="false" android:includeFontPadding="false"
android:text="/500자" android:text="/500"
android:textColor="@color/gray_500" /> android:textColor="@color/gray_500" />
</LinearLayout> </LinearLayout>
@@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="54dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/btn_back"
android:layout_width="42dp"
android:layout_height="match_parent"
android:contentDescription="@null"
android:padding="@dimen/spacing_12"
android:src="@drawable/ic_back" />
<LinearLayout
android:id="@+id/search_container"
android:layout_width="0dp"
android:layout_height="42dp"
android:layout_marginEnd="@dimen/spacing_14"
android:layout_weight="1"
android:background="@drawable/bg_round_corner_10_232323"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_12">
<ImageView
android:id="@+id/iv_search"
android:layout_width="18dp"
android:layout_height="18dp"
android:contentDescription="@null"
android:src="@drawable/ic_title_search_black" />
<EditText
android:id="@+id/et_search_nickname"
style="@style/Typography.Body3"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/spacing_8"
android:layout_weight="1"
android:background="@null"
android:hint="@string/dm_recipient_search_hint"
android:importantForAutofill="no"
android:inputType="textPersonName"
android:maxLines="1"
android:padding="0dp"
android:textColor="@color/white"
android:textColorHint="@color/color_979797"
android:textCursorDrawable="@drawable/edit_text_cursor"
tools:ignore="LabelFor" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/tv_followers"
style="@style/Typography.Body1"
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center_vertical"
android:paddingHorizontal="@dimen/spacing_14"
android:text="@string/dm_recipient_followers"
android:textColor="@color/color_e2e2e2" />
<TextView
android:id="@+id/tv_search_result_count"
style="@style/Typography.Body1"
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center_vertical"
android:paddingHorizontal="@dimen/spacing_14"
android:text="@string/dm_recipient_search_result_count"
android:textColor="@color/white"
android:visibility="gone"
tools:text="검색결과 0" />
<FrameLayout
android:id="@+id/recipient_content_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_recipients"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/tv_search_empty"
style="@style/Typography.Body5"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:padding="@dimen/spacing_20"
android:text="@string/dm_recipient_search_empty"
android:textColor="@color/color_777777"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
@@ -26,7 +26,7 @@
android:fontFamily="@font/medium" android:fontFamily="@font/medium"
android:textColor="@color/color_eeeeee" android:textColor="@color/color_eeeeee"
android:textSize="14.7sp" android:textSize="14.7sp"
tools:text="보이스온 App 오픈 관련 공지사항입니다." /> tools:text="소다라이브 App 오픈 관련 공지사항입니다." />
<TextView <TextView
android:id="@+id/tv_date" android:id="@+id/tv_date"
@@ -31,7 +31,7 @@
android:textSize="18.3sp" android:textSize="18.3sp"
app:drawableStartCompat="@drawable/ic_back" app:drawableStartCompat="@drawable/ic_back"
tools:ignore="RelativeOverlap" tools:ignore="RelativeOverlap"
tools:text="보이스온" /> tools:text="소다라이브" />
</RelativeLayout> </RelativeLayout>
<ScrollView <ScrollView
+9 -7
View File
@@ -15,17 +15,19 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<TextView
style="@style/Typography.Heading3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/screen_splash_tagline"
android:textColor="@color/white" />
<ImageView <ImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/splash_text_logo" /> android:src="@drawable/splash_text_logo" />
<TextView
style="@style/Typography.Heading4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_8"
android:text="@string/screen_splash_tagline"
android:textColor="@color/white" />
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
+1 -1
View File
@@ -21,6 +21,6 @@
android:textSize="18.3sp" android:textSize="18.3sp"
app:drawableStartCompat="@drawable/ic_back" app:drawableStartCompat="@drawable/ic_back"
tools:ignore="RelativeOverlap" tools:ignore="RelativeOverlap"
tools:text="보이스온" /> tools:text="소다라이브" />
</RelativeLayout> </RelativeLayout>
@@ -16,6 +16,16 @@
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:background="@drawable/bg_bottom_sheet_handle" /> android:background="@drawable/bg_bottom_sheet_handle" />
<TextView
android:id="@+id/tv_channel_share"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/medium"
android:paddingVertical="16dp"
android:text="@string/screen_user_profile_share_channel"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView <TextView
android:id="@+id/tv_user_block" android:id="@+id/tv_user_block"
android:layout_width="match_parent" android:layout_width="match_parent"
+1 -1
View File
@@ -222,7 +222,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:fontFamily="@font/bold" android:fontFamily="@font/bold"
android:text="@string/voiceon_original_only" android:text="@string/sodalive_original_only"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="24sp" /> android:textSize="24sp" />
+5 -3
View File
@@ -291,18 +291,20 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginHorizontal="24dp" android:layout_marginHorizontal="24dp"
android:layout_marginBottom="32dp" android:adjustViewBounds="true"
android:layout_marginBottom="@dimen/spacing_32"
android:contentDescription="@null" android:contentDescription="@null"
android:src="@drawable/img_apply_creator" /> android:src="@drawable/img_apply_creator" />
<ImageView <ImageView
android:id="@+id/iv_introduce_voiceon" android:id="@+id/iv_introduce_sodalive"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:adjustViewBounds="true"
android:layout_marginHorizontal="24dp" android:layout_marginHorizontal="24dp"
android:contentDescription="@null" android:contentDescription="@null"
android:src="@drawable/img_introduce_voiceon" /> android:src="@drawable/img_introduce_sodalive" />
<!-- Recent Content Section --> <!-- Recent Content Section -->
<LinearLayout <LinearLayout
@@ -58,4 +58,17 @@
tools:text="아직 나눈 대화가 없어요.\n크리에이터와 이야기를 시작해 보세요." /> tools:text="아직 나눈 대화가 없어요.\n크리에이터와 이야기를 시작해 보세요." />
</FrameLayout> </FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<ImageButton
android:id="@+id/btn_new_dm"
android:layout_width="66dp"
android:layout_height="66dp"
android:layout_margin="@dimen/spacing_14"
android:background="@drawable/bg_creator_channel_owner_fab"
android:contentDescription="@string/screen_chat_floating_button"
android:elevation="8dp"
android:padding="@dimen/spacing_14"
android:src="@drawable/ic_new_talk_add"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
@@ -77,7 +77,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="2dp" android:layout_marginStart="2dp"
android:textColor="@color/white" android:textColor="@color/white"
tools:text="20캔" /> tools:text="20" />
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>

Some files were not shown because too many files have changed in this diff Show More