-
Notifications
You must be signed in to change notification settings - Fork 313
Merge develop to v7 (30.03.2026)
#6309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v7
Are you sure you want to change the base?
Changes from all commits
de7779c
3945757
13e5f79
6c61cbd
aa4d913
cdccfd9
f9084ff
4924fd1
6c63890
a7b8580
45c9f28
0f268bd
2bd5fe8
d33c0ef
331237f
cb66033
3d8989f
117e375
9db7522
01cf1ed
389faba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -74,6 +74,8 @@ import io.getstream.chat.android.client.attachment.prepareForUpload | |
| import io.getstream.chat.android.client.audio.AudioPlayer | ||
| import io.getstream.chat.android.client.audio.NativeMediaPlayerImpl | ||
| import io.getstream.chat.android.client.audio.StreamAudioPlayer | ||
| import io.getstream.chat.android.client.cdn.CDN | ||
| import io.getstream.chat.android.client.cdn.internal.StreamMediaDataSource | ||
| import io.getstream.chat.android.client.channel.ChannelClient | ||
| import io.getstream.chat.android.client.channel.state.ChannelStateLogicProvider | ||
| import io.getstream.chat.android.client.clientstate.DisconnectCause | ||
|
|
@@ -104,6 +106,7 @@ import io.getstream.chat.android.client.extensions.ATTACHMENT_TYPE_FILE | |
| import io.getstream.chat.android.client.extensions.ATTACHMENT_TYPE_IMAGE | ||
| import io.getstream.chat.android.client.extensions.cidToTypeAndId | ||
| import io.getstream.chat.android.client.extensions.extractBaseUrl | ||
| import io.getstream.chat.android.client.extensions.getCreatedAtOrNull | ||
| import io.getstream.chat.android.client.extensions.internal.hasPendingAttachments | ||
| import io.getstream.chat.android.client.extensions.internal.isLaterThanDays | ||
| import io.getstream.chat.android.client.header.VersionPrefixHeader | ||
|
|
@@ -162,6 +165,7 @@ import io.getstream.chat.android.client.user.storage.SharedPreferencesCredential | |
| import io.getstream.chat.android.client.user.storage.UserCredentialStorage | ||
| import io.getstream.chat.android.client.utils.ProgressCallback | ||
| import io.getstream.chat.android.client.utils.TokenUtils | ||
| import io.getstream.chat.android.client.utils.internal.ServerClockOffset | ||
| import io.getstream.chat.android.client.utils.mergePartially | ||
| import io.getstream.chat.android.client.utils.message.ensureId | ||
| import io.getstream.chat.android.client.utils.observable.ChatEventsObservable | ||
|
|
@@ -289,9 +293,13 @@ internal constructor( | |
| @InternalStreamChatApi | ||
| public val audioPlayer: AudioPlayer, | ||
| private val now: () -> Date = ::Date, | ||
| @InternalStreamChatApi | ||
| public val serverClockOffset: ServerClockOffset, | ||
| private val repository: ChatClientRepository, | ||
| private val messageReceiptReporter: MessageReceiptReporter, | ||
| internal val messageReceiptManager: MessageReceiptManager, | ||
| @InternalStreamChatApi | ||
| public val cdn: CDN? = null, | ||
| ) { | ||
| private val logger by taggedLogger(TAG) | ||
| private val fileManager = StreamFileManager() | ||
|
|
@@ -353,6 +361,7 @@ internal constructor( | |
| * | ||
| * @see [Plugin] | ||
| */ | ||
| @Volatile | ||
| @InternalStreamChatApi | ||
| public var plugins: List<Plugin> = emptyList() | ||
|
|
||
|
|
@@ -399,12 +408,16 @@ internal constructor( | |
| @Suppress("ThrowsCount") | ||
| internal inline fun <reified P : DependencyResolver, reified T : Any> resolvePluginDependency(): T { | ||
| StreamLog.v(TAG) { "[resolvePluginDependency] P: ${P::class.simpleName}, T: ${T::class.simpleName}" } | ||
| // Snapshot plugins BEFORE checking initializationState to avoid a race with disconnect(). | ||
| // disconnect() sets initializationState to NOT_INITIALIZED before clearing plugins, | ||
| // so if we snapshot plugins first and then see COMPLETE, the snapshot is guaranteed valid. | ||
| val currentPlugins = plugins | ||
| val initState = awaitInitializationState(RESOLVE_DEPENDENCY_TIMEOUT) | ||
| if (initState != InitializationState.COMPLETE) { | ||
| StreamLog.e(TAG) { "[resolvePluginDependency] failed (initializationState is not COMPLETE): $initState " } | ||
| throw IllegalStateException("ChatClient::connectUser() must be called before resolving any dependency") | ||
| } | ||
| val resolver = plugins.find { plugin -> | ||
| val resolver = currentPlugins.find { plugin -> | ||
|
Comment on lines
+411
to
+420
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The early If Line 414 runs while the client is still 🤖 Prompt for AI Agents |
||
| plugin is P | ||
| } ?: throw IllegalStateException( | ||
| "Plugin '${P::class.qualifiedName}' was not found. Did you init it within ChatClient?", | ||
|
|
@@ -1569,9 +1582,9 @@ internal constructor( | |
|
|
||
| notifications.onLogout() | ||
| // Set initializationState to NOT_INITIALIZED BEFORE clearing plugins to prevent race condition. | ||
| // This ensures the StatePlugin extension methods don't access the plugin during disconnect. | ||
| // resolvePluginDependency() snapshots plugins before checking state, so if it sees COMPLETE | ||
| // here, the snapshot is guaranteed to still contain the plugins. | ||
| mutableClientState.setInitializationState(InitializationState.NOT_INITIALIZED) | ||
|
|
||
| plugins.forEach { it.onUserDisconnected() } | ||
| plugins = emptyList() | ||
| userStateService.onLogout() | ||
|
|
@@ -2534,16 +2547,34 @@ internal constructor( | |
|
|
||
| /** | ||
| * Ensure the message has a [Message.createdLocallyAt] timestamp. | ||
| * If not, set it to the max of the channel's [Channel.lastMessageAt] + 1 millisecond and [now]. | ||
| * This ensures that the message appears in the correct order in the channel. | ||
| * If not, set it to the max of the channel's [Channel.lastMessageAt] + 1 millisecond and the | ||
| * estimated server time. Using estimated server time (instead of raw local clock) prevents | ||
| * cross-user ordering issues when the device clock is skewed. | ||
| */ | ||
| private suspend fun Message.ensureCreatedLocallyAt(cid: String): Message { | ||
| val lastMessageAt = repositoryFacade.selectChannel(cid = cid)?.lastMessageAt | ||
| val lastMessageAtPlusOneMillisecond = lastMessageAt?.let { | ||
| Date(it.time + 1) | ||
| val parentId = this.parentId | ||
| if (parentId != null) { | ||
| // Thread reply | ||
| val lastMessage = repositoryFacade.selectMessagesForThread(parentId, limit = 1).lastOrNull() | ||
| val lastMessageAt = lastMessage?.getCreatedAtOrNull() | ||
| val lastMessageAtPlusOneMillisecond = lastMessageAt?.let { | ||
| Date(it.time + 1) | ||
| } | ||
| val createdLocallyAt = max(lastMessageAtPlusOneMillisecond, serverClockOffset.estimatedServerTime()) | ||
| return copy(createdLocallyAt = this.createdLocallyAt ?: createdLocallyAt) | ||
| } else { | ||
| // Regular message | ||
| val (type, id) = cid.cidToTypeAndId() | ||
| // Fetch channel lastMessageAt from state, fallback to offline storage | ||
| val channelState = logicRegistry?.channelStateLogic(type, id)?.channelState() | ||
| val lastMessageAt = channelState?.channelData?.value?.lastMessageAt | ||
| ?: repositoryFacade.selectChannel(cid = cid)?.lastMessageAt | ||
| val lastMessageAtPlusOneMillisecond = lastMessageAt?.let { | ||
| Date(it.time + 1) | ||
| } | ||
| val createdLocallyAt = max(lastMessageAtPlusOneMillisecond, serverClockOffset.estimatedServerTime()) | ||
| return copy(createdLocallyAt = this.createdLocallyAt ?: createdLocallyAt) | ||
| } | ||
| val createdLocallyAt = max(lastMessageAtPlusOneMillisecond, now()) | ||
| return copy(createdLocallyAt = this.createdLocallyAt ?: createdLocallyAt) | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -4608,6 +4639,7 @@ internal constructor( | |
| private var uploadAttachmentsNetworkType = UploadAttachmentsNetworkType.CONNECTED | ||
| private var fileTransformer: FileTransformer = NoOpFileTransformer | ||
| private var apiModelTransformers: ApiModelTransformers = ApiModelTransformers() | ||
| private var cdn: CDN? = null | ||
| private var appName: String? = null | ||
| private var appVersion: String? = null | ||
|
|
||
|
|
@@ -4736,7 +4768,11 @@ internal constructor( | |
| * | ||
| * @param shareFileDownloadRequestInterceptor Your [Interceptor] implementation for the share file download | ||
| * call. | ||
| * @deprecated Use [io.getstream.chat.android.client.cdn.CDN] instead. Configure a custom CDN via | ||
| * [io.getstream.chat.android.client.ChatClient.Builder.cdn] to provide headers and transform URLs | ||
| * for all image, file, and download requests. | ||
| */ | ||
| @Deprecated("Use CDN instead. Configure via ChatClient.Builder.cdn().") | ||
| public fun shareFileDownloadRequestInterceptor(shareFileDownloadRequestInterceptor: Interceptor): Builder { | ||
| this.shareFileDownloadRequestInterceptor = shareFileDownloadRequestInterceptor | ||
| return this | ||
|
|
@@ -4807,6 +4843,15 @@ internal constructor( | |
| forceWsUrl = value | ||
| } | ||
|
|
||
| /** | ||
| * Sets a custom [CDN] implementation to be used by the client. | ||
| * | ||
| * @param cdn The custom CDN implementation. | ||
| */ | ||
| public fun cdn(cdn: CDN): Builder = apply { | ||
| this.cdn = cdn | ||
| } | ||
|
|
||
| /** | ||
| * Sets the CDN URL to be used by the client. | ||
| */ | ||
|
|
@@ -4933,6 +4978,8 @@ internal constructor( | |
| warmUpReflection() | ||
| } | ||
|
|
||
| val serverClockOffset = ServerClockOffset() | ||
|
|
||
| val module = | ||
| ChatModule( | ||
| appContext = appContext, | ||
|
|
@@ -4945,19 +4992,22 @@ internal constructor( | |
| fileUploader = fileUploader, | ||
| sendMessageInterceptor = sendMessageInterceptor, | ||
| shareFileDownloadRequestInterceptor = shareFileDownloadRequestInterceptor, | ||
| cdn = cdn, | ||
| tokenManager = tokenManager, | ||
| customOkHttpClient = customOkHttpClient, | ||
| clientDebugger = clientDebugger, | ||
| lifecycle = lifecycle, | ||
| appName = this.appName, | ||
| appVersion = this.appVersion, | ||
| serverClockOffset = serverClockOffset, | ||
| ) | ||
|
|
||
| val api = module.api() | ||
| val appSettingsManager = AppSettingManager(api) | ||
|
|
||
| val mediaDataSourceFactory = StreamMediaDataSource.factory(appContext, cdn) | ||
| val audioPlayer: AudioPlayer = StreamAudioPlayer( | ||
| mediaPlayer = NativeMediaPlayerImpl(appContext) { | ||
| mediaPlayer = NativeMediaPlayerImpl(mediaDataSourceFactory) { | ||
| ExoPlayer.Builder(appContext) | ||
| .setAudioAttributes( | ||
| AudioAttributes.Builder() | ||
|
|
@@ -4991,6 +5041,7 @@ internal constructor( | |
| retryPolicy = retryPolicy, | ||
| appSettingsManager = appSettingsManager, | ||
| chatSocket = module.chatSocket, | ||
| serverClockOffset = serverClockOffset, | ||
| pluginFactories = allPluginFactories, | ||
| repositoryFactoryProvider = allPluginFactories | ||
| .filterIsInstance<RepositoryFactory.Provider>() | ||
|
|
@@ -5011,6 +5062,7 @@ internal constructor( | |
| messageReceiptRepository = repository, | ||
| api = api, | ||
| ), | ||
| cdn = cdn, | ||
| ).apply { | ||
| attachmentsSender = AttachmentsSender( | ||
| context = appContext, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add alt text to the hero image for accessibility.
The image lacks an
altattribute, which impacts accessibility for screen reader users. Static analysis (markdownlint MD045) flagged this.Proposed fix
📝 Committable suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)
[warning] 5-5: Images should have alternate text (alt text)
(MD045, no-alt-text)
🤖 Prompt for AI Agents