From d786211b659f457e34001765614582587f9690ae Mon Sep 17 00:00:00 2001
From: Ivan Sekovanikj <31964049+isekovanic@users.noreply.github.com>
Date: Thu, 9 Jul 2026 23:40:41 +0200
Subject: [PATCH 1/9] fix: yarn workspaces ci issues (#3724)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## ๐ฏ Goal
This PR fixes a regression introduced with [this
change](https://github.com/GetStream/stream-chat-react-native/pull/3717).
Namely, restoring the functionality in #3717 essentially forced
[this](https://github.com/GetStream/stream-chat-react-native/commit/e19a1864891b0d6c1d33e9c615cc85058e26e09e)
to happen as well. We in fact want this to only be the case whenever we
release a new tarball to `npm` and not in every scenario.
While bumping `version` is fine in `package.json`, workspace
dependencies need to be resolved through the `workspace:^` protocol.
## ๐ Implementation details
## ๐จ UI Changes
iOS
Before
After
Android
Before
After
## ๐งช Testing
## โ๏ธ Checklist
- [ ] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [ ] PR targets the `develop` branch
- [ ] Documentation is updated
- [ ] New code is tested in main example apps, including all possible
scenarios
- [ ] SampleApp iOS and Android
- [ ] Expo iOS and Android
---
package/bin/before-tag.sh | 15 +++++++++++----
package/bin/release.sh | 18 ++++++++++++++++--
package/expo-package/package.json | 2 +-
package/native-package/package.json | 2 +-
4 files changed, 29 insertions(+), 8 deletions(-)
diff --git a/package/bin/before-tag.sh b/package/bin/before-tag.sh
index 893161faaa..b99e8b1a39 100644
--- a/package/bin/before-tag.sh
+++ b/package/bin/before-tag.sh
@@ -10,15 +10,22 @@ set -eux
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]')
PACKAGE_VERSION_WITHOUT_SHA=$(echo "$PACKAGE_VERSION" | cut -d"+" -f1)
+# We bump ONLY the `version` field of the internal packages here and deliberately leave their
+# `stream-chat-react-native-core` dependency as `workspace:^`. This script runs during
+# semantic-release's *prepare* step, before the release commit + `git push origin main` (see
+# `release/prod.js`), so whatever these files look like on disk here is what gets committed.
+#
+# Bumping `version` is safe to commit: Yarn resolves workspace packages to a `0.0.0-use.local`
+# placeholder in the lockfile and keys on the `workspace:^` descriptor, so the version field
+# never affects yarn.lock. Resolving `workspace:^` to a concrete version, however, WOULD change
+# the lockfile descriptor and break `yarn install --immutable` and local workspace linking.
+# That resolution is therefore done only in the published npm tarball, at publish time, in
+# `release.sh` (which runs after this commit) and is never committed back to git.
cd native-package
npm version --no-workspaces --allow-same-version --no-git-tag-version "$PACKAGE_VERSION_WITHOUT_SHA"
-sed -e 's|"stream-chat-react-native-core": "[^"]*"|"stream-chat-react-native-core": "'"$PACKAGE_VERSION_WITHOUT_SHA"'"|g' -i.bak package.json
-rm package.json.bak
cd ../expo-package
npm version --no-workspaces --allow-same-version --no-git-tag-version "$PACKAGE_VERSION_WITHOUT_SHA"
-sed -e 's|"stream-chat-react-native-core": "[^"]*"|"stream-chat-react-native-core": "'"$PACKAGE_VERSION_WITHOUT_SHA"'"|g' -i.bak package.json
-rm package.json.bak
cd ..
sed -e 's|"version": "[^"]*"|"version": "'"$PACKAGE_VERSION_WITHOUT_SHA"'"|g' -i.bak src/version.json
diff --git a/package/bin/release.sh b/package/bin/release.sh
index f17991bc16..986973a0e2 100755
--- a/package/bin/release.sh
+++ b/package/bin/release.sh
@@ -6,21 +6,35 @@
set -eux
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]')
+PACKAGE_VERSION_WITHOUT_SHA=$(echo "$PACKAGE_VERSION" | cut -d"+" -f1)
PACKAGE_TAG=$(sed 's/.*-\(.*\)\..*/\1/' <<< "$PACKAGE_VERSION")
+# Resolve the `workspace:^` protocol to the concrete published version. npm cannot understand
+# Yarn's `workspace:` protocol, so the tarball must carry a real version. We do this ONLY here,
+# at publish time as this runs after `semantic-release` has already committed and pushed the release
+# commit (see release/prod.js), on a throwaway CI checkout, so the rewrite is never committed.
+# That keeps `workspace:^` intact in git for local dev and `yarn install --immutable`, while the
+# published packages still depend on a concrete `stream-chat-react-native-core` version.
+resolve_workspace_dep() {
+ sed -e 's|"stream-chat-react-native-core": "[^"]*"|"stream-chat-react-native-core": "'"$PACKAGE_VERSION_WITHOUT_SHA"'"|g' -i.bak package.json
+ rm package.json.bak
+}
+
# If tag === version it means that its not a prerelease and shouuld set things to latest
if [[ "${PACKAGE_TAG}" != "${PACKAGE_VERSION}" ]]; then
cd native-package
+ resolve_workspace_dep
npm publish --no-workspaces --tag="$PACKAGE_TAG"
cd ../expo-package
+ resolve_workspace_dep
npm publish --no-workspaces --tag="$PACKAGE_TAG"
else
cd native-package
+ resolve_workspace_dep
npm publish --no-workspaces
cd ../expo-package
+ resolve_workspace_dep
npm publish --no-workspaces
fi
-
-
diff --git a/package/expo-package/package.json b/package/expo-package/package.json
index ffc9f5071b..2bc19bd2b9 100644
--- a/package/expo-package/package.json
+++ b/package/expo-package/package.json
@@ -27,7 +27,7 @@
"types": "types/index.d.ts",
"dependencies": {
"mime": "^4.0.7",
- "stream-chat-react-native-core": "9.6.1"
+ "stream-chat-react-native-core": "workspace:^"
},
"peerDependencies": {
"expo": ">=52.0.0",
diff --git a/package/native-package/package.json b/package/native-package/package.json
index befce3a703..d7ba58849c 100644
--- a/package/native-package/package.json
+++ b/package/native-package/package.json
@@ -30,7 +30,7 @@
"dependencies": {
"es6-symbol": "^3.1.3",
"mime": "^4.0.7",
- "stream-chat-react-native-core": "9.6.1"
+ "stream-chat-react-native-core": "workspace:^"
},
"peerDependencies": {
"@react-native-camera-roll/camera-roll": ">=7.9.0",
From c79da0069c3eeb90b17b20e282c817010a66d8df Mon Sep 17 00:00:00 2001
From: Oliver Lazoroski
Date: Fri, 10 Jul 2026 11:52:56 +0200
Subject: [PATCH 2/9] ci: restore SampleApp Android APK upload to S3 (#3725)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## ๐ฏ Goal
Restore the public download link for the SampleApp Android build:
`https://getstream.io/downloads/rn-sample-app.apk`.
This S3 upload was removed in #3334 ("chore: add android build and
deploy workflows and improve ios workflow"), which replaced the S3-based
Android job with Firebase App Distribution. Firebase serves internal QA
testers, but it dropped the public, unauthenticated download link that
lives on our own domain. This PR brings that link back **alongside**
Firebase.
## ๐ Implementation details
Re-adds the S3 upload as two steps in the existing
`build_and_deploy_android_firebase` job in
[`sample-distribution.yml`](.github/workflows/sample-distribution.yml):
- **Configure AWS credentials** โ
`aws-actions/configure-aws-credentials@v4` (the previous code used the
now-EOL `v1`), authenticating with the `AWS_ACCESS_KEY_ID` /
`AWS_SECRET_ACCESS_KEY` secrets.
- **Upload APK to S3** โ copies the APK to `rn-sample-app.apk` and runs
`aws s3 cp ... --sse AES256`.
Notable choices:
- **No rebuild.** The upload reuses the APK the Fastlane step already
produces at `examples/SampleApp/app-build/reactnativesampleapp.apk`, so
the previous manual `react-native bundle` + `gradlew assembleRelease`
steps are not reintroduced.
- **Object key unchanged** (`rn-sample-app.apk`) so the existing public
URL keeps working.
- **`develop`-only**, matching the original behavior.
- **Firebase distribution is kept** โ this is additive.
## ๐จ UI Changes
N/A โ CI/workflow-only change, no UI impact.
## ๐งช Testing
CI-only change; verified by the `develop` pipeline rather than the
example apps:
1. On merge to `develop`, the `build_and_deploy_android_firebase` job
runs the two new steps.
2. Confirm the `Upload APK to S3` step succeeds in the Actions logs.
3. Confirm `https://getstream.io/downloads/rn-sample-app.apk` downloads
the latest build.
## โ๏ธ Checklist
- [ ] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [x] PR targets the `develop` branch
- [ ] Documentation is updated
- [ ] New code is tested in main example apps, including all possible
scenarios
- [ ] SampleApp iOS and Android
- [ ] Expo iOS and Android
---
.github/workflows/sample-distribution.yml | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/.github/workflows/sample-distribution.yml b/.github/workflows/sample-distribution.yml
index c0a5b51255..d6179e2793 100644
--- a/.github/workflows/sample-distribution.yml
+++ b/.github/workflows/sample-distribution.yml
@@ -94,3 +94,19 @@ jobs:
env:
ANDROID_FIREBASE_APP_ID: ${{ secrets.ANDROID_FIREBASE_APP_ID }}
FIREBASE_CREDENTIALS_JSON: ${{ secrets.FIREBASE_CREDENTIALS_JSON }}
+ # Publish the same APK to S3 for the public download link. Reuses the
+ # artifact the Fastlane step above already built (app-build/), no rebuild.
+ - name: Configure AWS credentials
+ if: github.ref == 'refs/heads/develop'
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: us-east-1
+ - name: Upload APK to S3
+ if: github.ref == 'refs/heads/develop'
+ working-directory: examples/SampleApp
+ # Public download link: https://getstream.io/downloads/rn-sample-app.apk
+ run: |
+ cp app-build/reactnativesampleapp.apk rn-sample-app.apk
+ aws s3 cp rn-sample-app.apk s3://${{ secrets.AWS_S3_BUCKET }} --sse AES256
From 364d30a7886f709f68a8aa19ee92c1bf0619ccc1 Mon Sep 17 00:00:00 2001
From: Oliver Lazoroski
Date: Fri, 10 Jul 2026 12:25:31 +0200
Subject: [PATCH 3/9] ci: install AWS CLI on self-hosted runner for S3 upload
(#3726)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## ๐ฏ Goal
Follow-up to #3725 (merged), which restored the SampleApp Android APK
upload to S3 that powers
`https://getstream.io/downloads/rn-sample-app.apk`. That upload fails on
the self-hosted `public` runner because the AWS CLI is not installed
there (the original S3 job ran on GitHub-hosted `ubuntu-latest`, where
`aws` is preinstalled). This PR makes the job install the AWS CLI when
it is missing so the upload can succeed.
## ๐ Implementation details
Adds an `Ensure AWS CLI is available` step to the
`build_and_deploy_android_firebase` job in
[`sample-distribution.yml`](.github/workflows/sample-distribution.yml),
before the AWS credentials/upload steps:
- No-op when `aws` is already on `PATH` (so it costs nothing if the
runner image gains the CLI later).
- Otherwise installs AWS CLI v2 from the official bundle,
architecture-aware (`x86_64` / `aarch64`).
- Installs to a user-writable location (`$HOME/.local/...`) with no
`sudo`, then appends the bin dir to `$GITHUB_PATH` so the subsequent
`Upload APK to S3` step picks it up.
- Runs on `develop` only, matching the rest of the S3 flow.
## ๐จ UI Changes
N/A โ CI/workflow-only change, no UI impact.
## ๐งช Testing
CI-only change; verified by the `develop` pipeline rather than the
example apps:
1. On merge to `develop`, the `build_and_deploy_android_firebase` job
runs the new install step and logs the resolved `aws --version`.
2. Confirm the `Upload APK to S3` step now succeeds in the Actions logs.
3. Confirm `https://getstream.io/downloads/rn-sample-app.apk` serves the
latest build.
## โ๏ธ Checklist
- [ ] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [x] PR targets the `develop` branch
- [ ] Documentation is updated
- [ ] New code is tested in main example apps, including all possible
scenarios
- [ ] SampleApp iOS and Android
- [ ] Expo iOS and Android
---
.github/workflows/sample-distribution.yml | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/.github/workflows/sample-distribution.yml b/.github/workflows/sample-distribution.yml
index d6179e2793..363f9efd8e 100644
--- a/.github/workflows/sample-distribution.yml
+++ b/.github/workflows/sample-distribution.yml
@@ -96,6 +96,26 @@ jobs:
FIREBASE_CREDENTIALS_JSON: ${{ secrets.FIREBASE_CREDENTIALS_JSON }}
# Publish the same APK to S3 for the public download link. Reuses the
# artifact the Fastlane step above already built (app-build/), no rebuild.
+ - name: Ensure AWS CLI is available
+ if: github.ref == 'refs/heads/develop'
+ # The self-hosted `public` runner does not ship the AWS CLI. Install v2
+ # only when missing, to a user-writable dir (no sudo), then expose it.
+ run: |
+ if command -v aws >/dev/null 2>&1; then
+ echo "AWS CLI already installed: $(aws --version)"
+ exit 0
+ fi
+ echo "AWS CLI not found; installing v2..."
+ case "$(uname -m)" in
+ x86_64) zip="awscli-exe-linux-x86_64.zip" ;;
+ aarch64|arm64) zip="awscli-exe-linux-aarch64.zip" ;;
+ *) echo "Unsupported architecture: $(uname -m)"; exit 1 ;;
+ esac
+ curl -fsSL "https://awscli.amazonaws.com/${zip}" -o /tmp/awscliv2.zip
+ unzip -q /tmp/awscliv2.zip -d /tmp
+ /tmp/aws/install --bin-dir "$HOME/.local/bin" --install-dir "$HOME/.local/aws-cli" --update
+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
+ "$HOME/.local/bin/aws" --version
- name: Configure AWS credentials
if: github.ref == 'refs/heads/develop'
uses: aws-actions/configure-aws-credentials@v4
From eb94ff8d3832ecbd8c8ff4eb93197d6bc0f20db5 Mon Sep 17 00:00:00 2001
From: Zita Szupera
Date: Fri, 10 Jul 2026 10:06:54 -0500
Subject: [PATCH 4/9] fix: export OutputButtons from the SDK (#3727)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## ๐ฏ Goal
Docs PR: https://github.com/GetStream/docs-content/pull/1418
## ๐ Implementation details
## ๐จ UI Changes
iOS
Before
After
Android
Before
After
## ๐งช Testing
## โ๏ธ Checklist
- [ ] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [ ] PR targets the `develop` branch
- [ ] Documentation is updated
- [ ] New code is tested in main example apps, including all possible
scenarios
- [ ] SampleApp iOS and Android
- [ ] Expo iOS and Android
---
package/src/components/index.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/package/src/components/index.ts b/package/src/components/index.ts
index 58f07e9441..4f906d2d19 100644
--- a/package/src/components/index.ts
+++ b/package/src/components/index.ts
@@ -130,6 +130,7 @@ export * from './MessageInput/MessageInputFooterView';
export * from './MessageInput/MessageInputHeaderView';
export * from './MessageInput/MessageInputLeadingView';
export * from './MessageInput/MessageInputTrailingView';
+export * from './MessageInput/components/OutputButtons';
export * from './MessageInput/components/OutputButtons/SendButton';
export * from './MessageInput/SendMessageDisallowedIndicator';
export * from './MessageInput/ShowThreadMessageInChannelButton';
From dbecc8ef6d12506cd1a9fcbd3db6a7c15dcc862d Mon Sep 17 00:00:00 2001
From: Zita Szupera
Date: Fri, 10 Jul 2026 10:57:28 -0500
Subject: [PATCH 5/9] fix: allow moving text inside message content (#3723)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## ๐ฏ Goal
Even though we had for reordering message content (text, gallery, poll
etc) text had a fixed position. This PR enables updating text's position
too.
https://github.com/GetStream/docs-content/pull/1417/changes
## ๐ Implementation details
## ๐จ UI Changes
No UI changes, the following URL contains before-after shots verifying
no change:
https://claude.ai/code/artifact/c071bb00-30f3-48ba-ac12-708204549007?via=auto_preview
(if the link doesn't work, try the attached screenshots)
[message-content-ordering-report.zip](https://github.com/user-attachments/files/29860209/message-content-ordering-report.zip)
## ๐งช Testing
Tested manually on android and ios too
## โ๏ธ Checklist
- [ ] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [ ] PR targets the `develop` branch
- [ ] Documentation is updated
- [ ] New code is tested in main example apps, including all possible
scenarios
- [ ] SampleApp iOS and Android
- [ ] Expo iOS and Android
---
package/src/components/Channel/Channel.tsx | 2 +-
.../MessageItemView/MessageContent.tsx | 28 ++-
.../MessageItemView/MessageTextContainer.tsx | 2 +-
.../__tests__/MessageContent.test.tsx | 116 +++++++++
.../MessageTextContainer.test.tsx.snap | 2 +-
.../__snapshots__/Thread.test.tsx.snap | 236 +++++++++---------
6 files changed, 263 insertions(+), 123 deletions(-)
diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx
index 4e4848ca7b..1c7bfe054e 100644
--- a/package/src/components/Channel/Channel.tsx
+++ b/package/src/components/Channel/Channel.tsx
@@ -468,8 +468,8 @@ const ChannelWithContext = (props: PropsWithChildren) =
'poll',
'ai_text',
'attachments',
- 'text',
'location',
+ 'text',
],
messageOverlayTargetId,
messageInputFloating = false,
diff --git a/package/src/components/Message/MessageItemView/MessageContent.tsx b/package/src/components/Message/MessageItemView/MessageContent.tsx
index 8f699f06ee..57787cd56c 100644
--- a/package/src/components/Message/MessageItemView/MessageContent.tsx
+++ b/package/src/components/Message/MessageItemView/MessageContent.tsx
@@ -220,12 +220,25 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
const { setNativeScrollability } = useMessageListItemContext();
const hasContentSideViews = !!(MessageContentLeadingView || MessageContentTrailingView);
+ const gap = primitives.spacingXs;
+
+ const messageTextContainerStyles = useMemo(() => {
+ return {
+ textContainer: {
+ // Cancel the container's 8px inter-item gap so only the caption's own
+ // paragraph marginTop shows. Skip for the first item: there is no gap to
+ // cancel
+ marginTop: -gap,
+ },
+ };
+ }, [gap]);
+
const contentBody = (
<>
{
key={`ai_message_text_container_${messageContentOrderIndex}`}
/>
) : null;
+ case 'text': {
+ const suppressed =
+ (otherAttachments.length && otherAttachments[0].actions) || isAIGenerated;
+ return suppressed ? null : (
+
+ );
+ }
default:
return null;
}
})}
- {(otherAttachments.length && otherAttachments[0].actions) || isAIGenerated ? null : (
-
- )}
>
);
const a11yPressableLabel = useMemo(() => {
diff --git a/package/src/components/Message/MessageItemView/MessageTextContainer.tsx b/package/src/components/Message/MessageItemView/MessageTextContainer.tsx
index ff01cc5d04..32b00d37a3 100644
--- a/package/src/components/Message/MessageItemView/MessageTextContainer.tsx
+++ b/package/src/components/Message/MessageItemView/MessageTextContainer.tsx
@@ -21,7 +21,7 @@ import { useTranslatedMessage } from '../../../hooks/useTranslatedMessage';
import { primitives } from '../../../theme';
const styles = StyleSheet.create({
- textContainer: { maxWidth: 256, paddingHorizontal: primitives.spacingSm },
+ textContainer: { maxWidth: 256, paddingHorizontal: primitives.spacingXxs },
});
export type MessageTextProps = MessageTextContainerProps & {
diff --git a/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.tsx b/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.tsx
index 7d44fbe0d3..bac4032f1a 100644
--- a/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.tsx
+++ b/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.tsx
@@ -445,6 +445,122 @@ describe('MessageContent', () => {
});
});
+ // Collects the given testIDs in the order they appear in the rendered tree.
+ const collectOrderedTestIDs = (
+ root: ReturnType,
+ targets: string[],
+ ) => {
+ const found: string[] = [];
+ const walk = (node: typeof root | string | null) => {
+ if (!node || typeof node === 'string') {
+ return;
+ }
+ const testID = node.props?.testID;
+ if (testID && targets.includes(testID) && !found.includes(testID)) {
+ found.push(testID);
+ }
+ (node.children ?? []).forEach((child) => walk(child as typeof root | string));
+ };
+ walk(root);
+ return found;
+ };
+
+ it('renders text after attachments with the default messageContentOrder', async () => {
+ const user = generateUser();
+ const message = generateMessage({
+ attachments: [
+ { image_url: 'https://i.imgur.com/SLx06PP.png', type: 'image' },
+ { image_url: 'https://i.imgur.com/iNaC3K7.jpg', type: 'image' },
+ ],
+ text: 'a caption',
+ user,
+ });
+
+ renderMessage({ message });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('gallery-container')).toBeTruthy();
+ expect(screen.getByTestId('message-text-container')).toBeTruthy();
+ });
+
+ expect(
+ collectOrderedTestIDs(screen.getByTestId('message-content-wrapper'), [
+ 'gallery-container',
+ 'message-text-container',
+ ]),
+ ).toEqual(['gallery-container', 'message-text-container']);
+ });
+
+ it('renders text before attachments when messageContentOrder puts text first', async () => {
+ const user = generateUser();
+ const message = generateMessage({
+ attachments: [
+ { image_url: 'https://i.imgur.com/SLx06PP.png', type: 'image' },
+ { image_url: 'https://i.imgur.com/iNaC3K7.jpg', type: 'image' },
+ ],
+ text: 'a caption',
+ user,
+ });
+
+ render(
+
+
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('gallery-container')).toBeTruthy();
+ expect(screen.getByTestId('message-text-container')).toBeTruthy();
+ });
+
+ expect(
+ collectOrderedTestIDs(screen.getByTestId('message-content-wrapper'), [
+ 'gallery-container',
+ 'message-text-container',
+ ]),
+ ).toEqual(['message-text-container', 'gallery-container']);
+ });
+
+ it('suppresses the text container for an ephemeral giphy preview with actions', async () => {
+ const user = generateUser();
+ const message = generateMessage({
+ attachments: [
+ {
+ ...generateGiphyAttachment(),
+ actions: [
+ generateAttachmentAction(),
+ generateAttachmentAction(),
+ generateAttachmentAction(),
+ ],
+ },
+ ],
+ text: '/giphy hello',
+ user,
+ });
+
+ renderMessage({ message });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('giphy-action-attachment')).toBeTruthy();
+ });
+
+ expect(screen.queryByTestId('message-text-container')).toBeFalsy();
+ });
+
it('renders the ReactionList when the message has reactions', async () => {
const user = generateUser();
const reaction = generateReaction();
diff --git a/package/src/components/Message/MessageItemView/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap b/package/src/components/Message/MessageItemView/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap
index 99b414f7a2..f016eb6247 100644
--- a/package/src/components/Message/MessageItemView/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap
+++ b/package/src/components/Message/MessageItemView/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap
@@ -6,7 +6,7 @@ exports[`MessageTextContainer should render message text container 1`] = `
[
{
"maxWidth": 256,
- "paddingHorizontal": 12,
+ "paddingHorizontal": 4,
},
{},
undefined,
diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap
index 0881547ce7..a477d74f15 100644
--- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap
+++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap
@@ -530,57 +530,58 @@ exports[`Thread should match thread snapshot 1`] = `
{},
]
}
- />
-
-
- Message6
+
+ Message6
+
-
+
@@ -864,57 +865,58 @@ exports[`Thread should match thread snapshot 1`] = `
{},
]
}
- />
-
-
- Message5
+
+ Message5
+
-
+
@@ -1232,57 +1234,58 @@ exports[`Thread should match thread snapshot 1`] = `
{},
]
}
- />
-
-
- Message4
+
+ Message4
+
-
+
@@ -1560,57 +1563,58 @@ exports[`Thread should match thread snapshot 1`] = `
{},
]
}
- />
-
-
- Message3
+
+ Message3
+
-
+
From a740c7a641e205edff116736e5236838af2a8cae Mon Sep 17 00:00:00 2001
From: Zita Szupera
Date: Mon, 13 Jul 2026 10:01:28 -0500
Subject: [PATCH 6/9] fix: theming fixes (avatar, reaction pill, force message
alignment) (#3728)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## ๐ฏ Goal
A few small fixes for issues spotted during AI skill tests:
- `avatar` theming key wasn't used and had an outdated shape. It's
technically a breaking change, but since the shape is broken currently,
it should be fine
- Adds theme keys to fully style the reaction pill's selected and
pressed states (previously only the background could've been changed)
- Two small fix for `forceAlignMessages="'left'"` setting
## ๐ Implementation details
## ๐จ UI Changes
iOS
Before
After
Android
Before
After
## ๐งช Testing
## โ๏ธ Checklist
- [ ] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [ ] PR targets the `develop` branch
- [ ] Documentation is updated
- [ ] New code is tested in main example apps, including all possible
scenarios
- [ ] SampleApp iOS and Android
- [ ] Expo iOS and Android
---
.../MessageItemView/MessageContent.tsx | 4 ++--
.../MessageItemView/MessageItemView.tsx | 2 +-
.../ReactionList/ReactionListItemWrapper.tsx | 12 ++++++++--
package/src/components/ui/Avatar/Avatar.tsx | 10 +++++---
.../src/components/ui/Avatar/AvatarGroup.tsx | 2 --
.../src/contexts/themeContext/utils/theme.ts | 24 ++++++-------------
6 files changed, 27 insertions(+), 27 deletions(-)
diff --git a/package/src/components/Message/MessageItemView/MessageContent.tsx b/package/src/components/Message/MessageItemView/MessageContent.tsx
index 57787cd56c..97267a3876 100644
--- a/package/src/components/Message/MessageItemView/MessageContent.tsx
+++ b/package/src/components/Message/MessageItemView/MessageContent.tsx
@@ -190,7 +190,7 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
let computedBottomRightRadius = components.messageBubbleRadiusGroupBottom;
if (isBottomOrSingle) {
// add relevant sharp corner (the "tail")
- if (isMyMessage) {
+ if (alignment === 'right') {
computedBottomRightRadius = components.messageBubbleRadiusTail;
} else {
computedBottomLeftRadius = components.messageBubbleRadiusTail;
@@ -208,6 +208,7 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
return style;
}, [
+ alignment,
backgroundColor,
borderBottomLeftRadius,
borderBottomRightRadius,
@@ -215,7 +216,6 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
borderTopLeftRadius,
borderTopRightRadius,
groupStyles,
- isMyMessage,
]);
const { setNativeScrollability } = useMessageListItemContext();
diff --git a/package/src/components/Message/MessageItemView/MessageItemView.tsx b/package/src/components/Message/MessageItemView/MessageItemView.tsx
index 28e7719d3a..4d9a842873 100644
--- a/package/src/components/Message/MessageItemView/MessageItemView.tsx
+++ b/package/src/components/Message/MessageItemView/MessageItemView.tsx
@@ -221,7 +221,7 @@ const MessageItemViewWithContext = (props: MessageItemViewPropsWithContext) => {
? semantics.backgroundUtilityPressed
: semantics.reactionBg,
},
+ selected && styles.containerSelected,
+ pressed && styles.containerPressed,
style,
]}
{...rest}
@@ -43,7 +45,7 @@ const useStyles = () => {
theme: {
semantics,
messageItemView: {
- reactionListItemWrapper: { wrapper, container },
+ reactionListItemWrapper: { wrapper, container, containerSelected, containerPressed },
},
},
} = useTheme();
@@ -61,11 +63,17 @@ const useStyles = () => {
gap: primitives.spacingXxs,
...container,
},
+ containerPressed: {
+ ...containerPressed,
+ },
+ containerSelected: {
+ ...containerSelected,
+ },
wrapper: {
backgroundColor: semantics.reactionBg,
borderRadius: primitives.radiusMax,
...wrapper,
},
});
- }, [semantics, wrapper, container]);
+ }, [semantics, wrapper, container, containerSelected, containerPressed]);
};
diff --git a/package/src/components/ui/Avatar/Avatar.tsx b/package/src/components/ui/Avatar/Avatar.tsx
index d5a64d0522..42df9afa3d 100644
--- a/package/src/components/ui/Avatar/Avatar.tsx
+++ b/package/src/components/ui/Avatar/Avatar.tsx
@@ -85,7 +85,7 @@ export const Avatar = (props: AvatarProps) => {
const useStyles = () => {
const {
- theme: { semantics },
+ theme: { avatar, semantics },
} = useTheme();
const { borderCoreOpacitySubtle } = semantics;
return useMemo(
@@ -94,15 +94,19 @@ const useStyles = () => {
border: {
borderColor: borderCoreOpacitySubtle,
borderWidth: 1,
+ ...avatar.border,
},
container: {
alignItems: 'center',
borderRadius: primitives.radiusMax,
justifyContent: 'center',
overflow: 'hidden',
+ ...avatar.container,
+ },
+ image: {
+ ...avatar.image,
},
- image: {},
}),
- [borderCoreOpacitySubtle],
+ [avatar.border, avatar.container, avatar.image, borderCoreOpacitySubtle],
);
};
diff --git a/package/src/components/ui/Avatar/AvatarGroup.tsx b/package/src/components/ui/Avatar/AvatarGroup.tsx
index 1d272ea4c3..aae521f752 100644
--- a/package/src/components/ui/Avatar/AvatarGroup.tsx
+++ b/package/src/components/ui/Avatar/AvatarGroup.tsx
@@ -11,7 +11,6 @@ import { UserAvatar, UserAvatarProps } from './UserAvatar';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import { PeopleIcon } from '../../../icons/users';
-import { primitives } from '../../../theme';
import { BadgeCount, BadgeCountProps, OnlineIndicator, OnlineIndicatorProps } from '../Badge';
export type AvatarGroupProps = {
@@ -217,7 +216,6 @@ const useUserAvatarGroupStyles = () => {
userAvatarWrapper: {
borderWidth: 2,
borderColor: semantics.borderCoreInverse,
- borderRadius: primitives.radiusMax,
},
onlineIndicatorWrapper: {
position: 'absolute',
diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts
index 66ce7e8354..4bb03625c8 100644
--- a/package/src/contexts/themeContext/utils/theme.ts
+++ b/package/src/contexts/themeContext/utils/theme.ts
@@ -5,7 +5,6 @@ import {
type TextStyle,
type ViewStyle,
} from 'react-native';
-import type { CircleProps } from 'react-native-svg';
import type { IconProps } from '../../../icons/utils/base';
import { primitives, lightSemantics, darkSemantics } from '../../../theme';
@@ -139,11 +138,9 @@ export type Theme = {
speedChangeButtonText: TextStyle;
};
avatar: {
- BASE_AVATAR_SIZE: number;
+ border: ViewStyle;
container: ViewStyle;
image: ImageStyle;
- presenceIndicator: CircleProps;
- presenceIndicatorContainer: ViewStyle;
};
avatarStack: {
userAvatarWrapper: ViewStyle;
@@ -967,6 +964,8 @@ export type Theme = {
reactionListItemWrapper: {
wrapper: ViewStyle;
container: ViewStyle;
+ containerSelected: ViewStyle;
+ containerPressed: ViewStyle;
};
reactionListTop: {
container: ViewStyle;
@@ -1256,20 +1255,9 @@ export const defaultTheme: Theme = {
speedChangeButtonText: {},
},
avatar: {
- BASE_AVATAR_SIZE,
+ border: {},
container: {},
- image: {
- borderRadius: 16,
- height: 32,
- width: 32,
- },
- presenceIndicator: {
- cx: 6,
- cy: 6,
- r: 5,
- strokeWidth: 2,
- },
- presenceIndicatorContainer: {},
+ image: {},
},
avatarStack: {
userAvatarWrapper: {},
@@ -2062,6 +2050,8 @@ export const defaultTheme: Theme = {
reactionListItemWrapper: {
wrapper: {},
container: {},
+ containerSelected: {},
+ containerPressed: {},
},
reactionListTop: {
container: {},
From 0a38205cf9a19331a8f1fae2e976aa6ad610e032 Mon Sep 17 00:00:00 2001
From: Oliver Lazoroski
Date: Wed, 15 Jul 2026 15:07:44 +0200
Subject: [PATCH 7/9] fix: export missing base UI components and correct stale
JSDoc/comments (#3730)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## ๐ฏ Goal
Fix issues surfaced by a documentation audit of the RN v9 docs against
the SDK source: several documented base-UI primitives were not exported
from the package root, and a few source doc-comments were stale (and
were the origin of doc errors).
## ๐ Implementation details
**Missing package-root exports** (documented components/hooks that
failed to import from `stream-chat-react-native`):
- `components/ui/index.ts` โ export `Input`, `SpeedSettingsButton`,
`GiphyChip`
- `components/index.ts` โ export `EmptyList`, `EmptySearchResult`,
`SearchInput`, `SelectionCircle` (were in `UIComponents/index.ts` but
never re-exported, since `components/index.ts` cherry-picks
`UIComponents/*` rather than re-exporting the barrel), and
`useCooldownRemaining`
**Stale source doc-comments corrected** (each caused a corresponding
docs bug):
- `contexts/pollContext/pollContext.tsx` โ `usePollContext` error
message referenced a non-existent `useCreatePollContext` hook
- `contexts/attachmentPickerContext/AttachmentPickerContext.tsx` โ JSDoc
referenced a non-existent `setBottomInset` setter (the context exposes
only `topInset`/`bottomInset` values)
- `components/Chat/Chat.tsx` โ `useNativeMultipartUpload` JSDoc
`@default true` โ `@default false` (matches the runtime default)
- `contexts/channelsContext/ChannelsContext.tsx` โ `numberOfSkeletons`
comment default `6` โ `8` (matches the runtime default in
`ChannelList.tsx`)
## ๐จ UI Changes
None โ export additions and doc-comment corrections only. No runtime/UI
behavior change.
## ๐งช Testing
- `tsc --noEmit -p package/tsconfig.json` passes clean (0 errors) โ
confirms all new exports resolve without name collisions.
- No behavior changes; the exported symbols were already used
internally.
## โ๏ธ Checklist
- [ ] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [x] PR targets the `develop` branch
- [x] Documentation is updated (companion docs PR:
GetStream/docs-content#1435)
- [ ] New code is tested in main example apps, including all possible
scenarios
- [ ] SampleApp iOS and Android
- [ ] Expo iOS and Android
---
package/src/components/Chat/Chat.tsx | 2 +-
package/src/components/index.ts | 5 +++++
package/src/components/ui/index.ts | 3 +++
.../attachmentPickerContext/AttachmentPickerContext.tsx | 2 +-
package/src/contexts/channelsContext/ChannelsContext.tsx | 2 +-
package/src/contexts/pollContext/pollContext.tsx | 2 +-
6 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/package/src/components/Chat/Chat.tsx b/package/src/components/Chat/Chat.tsx
index 89df8a737e..6e9b3079cc 100644
--- a/package/src/components/Chat/Chat.tsx
+++ b/package/src/components/Chat/Chat.tsx
@@ -51,7 +51,7 @@ export type ChatProps = Pick &
* This only controls whether the native adapter gets installed by this Chat instance.
* It does not uninstall an adapter that was already installed on the client.
*
- * @default true
+ * @default false
*/
useNativeMultipartUpload?: boolean;
/**
diff --git a/package/src/components/index.ts b/package/src/components/index.ts
index 4f906d2d19..7195904f8d 100644
--- a/package/src/components/index.ts
+++ b/package/src/components/index.ts
@@ -148,6 +148,7 @@ export * from './MessageInput/components/AttachmentPreview/AudioAttachmentUpload
export * from './MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview';
export * from './MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview';
export * from './MessageInput/hooks/useAudioRecorder';
+export * from './MessageInput/hooks/useCooldownRemaining';
export * from './MessageList/DateHeader';
export * from './MessageList/hooks/useMessageList';
@@ -196,6 +197,10 @@ export * from './UIComponents/SvgAwareImage';
export * from './UIComponents/Spinner';
export * from './UIComponents/SwipableWrapper';
export * from './UIComponents/PortalWhileClosingView';
+export * from './UIComponents/EmptyList';
+export * from './UIComponents/EmptySearchResult';
+export * from './UIComponents/SearchInput';
+export * from './UIComponents/SelectionCircle';
export * from './Thread/Thread';
export * from './Thread/components/ThreadFooterComponent';
diff --git a/package/src/components/ui/index.ts b/package/src/components/ui/index.ts
index 7daf9d6d37..afe40c071a 100644
--- a/package/src/components/ui/index.ts
+++ b/package/src/components/ui/index.ts
@@ -2,3 +2,6 @@ export * from './Avatar';
export * from './VideoPlayIndicator';
export * from './Button';
export * from './Badge';
+export * from './Input/Input';
+export * from './SpeedSettingsButton';
+export * from './GiphyChip';
diff --git a/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx b/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx
index 063d1f6f05..71ccda2f50 100644
--- a/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx
+++ b/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx
@@ -22,7 +22,7 @@ export type AttachmentPickerContextValue = Pick<
> & {
/**
* `bottomInset` determine the height of the `AttachmentPicker` and the underlying shift to the `MessageList` when it is opened.
- * This can also be set via the `setBottomInset` function provided by the `useAttachmentPickerContext` hook.
+ * This can be set via the `bottomInset` prop on the `Channel` component.
*
* Please check [OverlayProvider](https://github.com/GetStream/stream-chat-react-native/wiki/Cookbook-v3.0#overlayprovider) section in Cookbook
* for more details.
diff --git a/package/src/contexts/channelsContext/ChannelsContext.tsx b/package/src/contexts/channelsContext/ChannelsContext.tsx
index 48e66f26aa..c6aa806d22 100644
--- a/package/src/contexts/channelsContext/ChannelsContext.tsx
+++ b/package/src/contexts/channelsContext/ChannelsContext.tsx
@@ -69,7 +69,7 @@ export type ChannelsContextValue = {
*/
maxUnreadCount: number;
/**
- * Number of skeletons that should show when loading. Default: 6
+ * Number of skeletons that should show when loading. Default: 8
*/
numberOfSkeletons: number;
/**
diff --git a/package/src/contexts/pollContext/pollContext.tsx b/package/src/contexts/pollContext/pollContext.tsx
index ec0ef46e8d..bfb1f07797 100644
--- a/package/src/contexts/pollContext/pollContext.tsx
+++ b/package/src/contexts/pollContext/pollContext.tsx
@@ -29,7 +29,7 @@ export const usePollContext = () => {
if (contextValue === DEFAULT_BASE_CONTEXT_VALUE && !isTestEnvironment()) {
throw new Error(
- 'The useCreatePollContext hook was called outside of the PollContext provider. Make sure you have configured the Poll component correctly - https://getstream.io/chat/docs/sdk/reactnative/basics/hello_stream_chat/#channel',
+ 'The usePollContext hook was called outside of the PollContext provider. Make sure you have configured the Poll component correctly - https://getstream.io/chat/docs/sdk/reactnative/basics/hello_stream_chat/#channel',
);
}
From 230cfa104d616f3753dc06bbe34640fd20868f15 Mon Sep 17 00:00:00 2001
From: Zita Szupera
Date: Thu, 16 Jul 2026 13:49:11 -0500
Subject: [PATCH 8/9] feat: allow custom icons (#3731)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## ๐ฏ Goal
Allow providing custom icons
Docs: https://github.com/GetStream/docs-content/pull/1438
Fixes https://github.com/GetStream/stream-chat-react-native/issues/3449
## ๐ Implementation details
## ๐จ UI Changes
iOS