diff --git a/.github/workflows/cron-checks.yml b/.github/workflows/cron-checks.yml
index e08f24b1f..39333b1a1 100644
--- a/.github/workflows/cron-checks.yml
+++ b/.github/workflows/cron-checks.yml
@@ -16,33 +16,13 @@ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
- build-test-app-and-frameworks:
- name: Build Test App and Frameworks
- runs-on: macos-15
- steps:
- - uses: actions/checkout@v4.1.1
- - uses: ./.github/actions/ruby-cache
- - uses: ./.github/actions/xcode-cache
- - name: Build
- run: bundle exec fastlane build_test_app_and_frameworks
- timeout-minutes: 60
- - uses: actions/upload-artifact@v4
- if: success()
- with:
- name: cache-derived-data
- path: |
- derived_data/Build/**/*.app
- derived_data/Build/**/*.xctestrun
- derived_data/Build/**/*.framework
-
test-e2e-debug:
name: Test E2E UI (Debug)
- needs: build-test-app-and-frameworks
strategy:
matrix:
include:
- - ios: 18.2
- xcode: 16.2
+ - ios: 18.3
+ xcode: 16.3
os: macos-15
device: "iPhone 16 Pro"
setup_runtime: false
@@ -64,10 +44,6 @@ jobs:
XCODE_VERSION: ${{ matrix.xcode }}
steps:
- uses: actions/checkout@v4.1.1
- - uses: actions/download-artifact@v4
- with:
- name: cache-derived-data
- path: derived_data/Build/
- uses: ./.github/actions/bootstrap
env:
INSTALL_ALLURE: true
@@ -80,10 +56,13 @@ jobs:
with:
version: ${{ matrix.ios }}
device: ${{ matrix.device }}
+ - name: Build
+ run: bundle exec fastlane build_test_app_and_frameworks
+ timeout-minutes: 60
- name: Launch Allure TestOps
run: bundle exec fastlane allure_launch cron:true
- name: Run UI Tests (Debug)
- run: bundle exec fastlane test_e2e_mock device:"${{ matrix.device }} (${{ matrix.ios }})" cron:true test_without_building:true
+ run: bundle exec fastlane test_e2e_mock device:"${{ matrix.device }} (${{ matrix.ios }})"
timeout-minutes: 90
- name: Allure TestOps Upload
if: success() || failure()
@@ -112,6 +91,8 @@ jobs:
strategy:
matrix:
include:
+ - xcode: 16.3
+ os: macos-15
- xcode: 16.2
os: macos-15
- xcode: 16.1
@@ -169,7 +150,7 @@ jobs:
slack:
name: Slack Report
runs-on: ubuntu-latest
- needs: [test-e2e-debug, build-test-app-and-frameworks, build-apps, build-old-xcode, automated-code-review]
+ needs: [test-e2e-debug, build-apps, build-old-xcode, automated-code-review]
if: failure() && github.event_name == 'schedule'
steps:
- uses: 8398a7/action-slack@v3
diff --git a/.github/workflows/smoke-checks.yml b/.github/workflows/smoke-checks.yml
index 9348c37e0..fcd9b08d5 100644
--- a/.github/workflows/smoke-checks.yml
+++ b/.github/workflows/smoke-checks.yml
@@ -20,7 +20,7 @@ concurrency:
env:
HOMEBREW_NO_INSTALL_CLEANUP: 1 # Disable cleanup for homebrew, we don't need it on CI
- IOS_SIMULATOR_DEVICE: "iPhone 16 Pro (18.2)"
+ IOS_SIMULATOR_DEVICE: "iPhone 16 Pro (18.3)"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_PR_NUM: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml
index e17d113af..1008ab9d7 100644
--- a/.github/workflows/testflight.yml
+++ b/.github/workflows/testflight.yml
@@ -9,6 +9,15 @@ on:
types: [published]
workflow_dispatch:
+ inputs:
+ release:
+ description: 'Build configuration'
+ required: true
+ default: 'Debug'
+ type: choice
+ options:
+ - Debug
+ - Release
env:
HOMEBREW_NO_INSTALL_CLEANUP: 1
@@ -32,7 +41,7 @@ jobs:
APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_PR_NUM: ${{ github.event.number }}
- run: bundle exec fastlane swiftui_testflight_build
+ run: bundle exec fastlane swiftui_testflight_build configuration:"${{ github.event.inputs.release }}"
- uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 86f0f2b93..3ceeb8959 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### 🔄 Changed
+# [4.79.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.79.0)
+_May 29, 2025_
+
+### ✅ Added
+- Add extra data to user display info [#819](https://github.com/GetStream/stream-chat-swiftui/pull/819)
+- Make message spacing in message list configurable [#830](https://github.com/GetStream/stream-chat-swiftui/pull/830)
+- Show time, relative date, weekday, or short date for last message in channel list and search [#833](https://github.com/GetStream/stream-chat-swiftui/pull/833)
+ - Set `ChannelListConfig.messageRelativeDateFormatEnabled` to true for enabling it
+- Add `MessageViewModel` to `MessageContainerView` to make it easier to customise presentation logic [#815](https://github.com/GetStream/stream-chat-swiftui/pull/815)
+- Add `MessageListConfig.messaeDisplayOptions.showOriginalTranslatedButton` to enable showing original text in translated message [#815](https://github.com/GetStream/stream-chat-swiftui/pull/815)
+- Add `Utils.originalTranslationsStore` to keep track of messages that should show the original text [#815](https://github.com/GetStream/stream-chat-swiftui/pull/815)
+- Add `ViewFactory.makeGalleryHeaderView` for customising header view in `GalleryView` [#837](https://github.com/GetStream/stream-chat-swiftui/pull/837)
+- Add `ViewFactory.makeVideoPlayerHeaderView` for customising header view in `VideoPlayerView` [#837](https://github.com/GetStream/stream-chat-swiftui/pull/837)
+- Add `Utils.messagePreviewFormatter` for customising message previews in lists [#839](https://github.com/GetStream/stream-chat-swiftui/pull/839)
+### 🐞 Fixed
+- Fix swipe to reply enabled when quoting a message is disabled [#824](https://github.com/GetStream/stream-chat-swiftui/pull/824)
+- Fix mark unread action not removed when read events are disabled [#823](https://github.com/GetStream/stream-chat-swiftui/pull/823)
+- Fix user mentions not working when commands are disabled [#826](https://github.com/GetStream/stream-chat-swiftui/pull/826)
+- Fix edit message action shown when user does not have permissions [#835](https://github.com/GetStream/stream-chat-swiftui/pull/835)
+- Fix error indicator not shown when editing a message fails [#840](https://github.com/GetStream/stream-chat-swiftui/pull/840)
+- Fix read indicator shown for failed edited messages [#840](https://github.com/GetStream/stream-chat-swiftui/pull/840)
+- Fix "clock" pending icon not shown when message is syncing (pending to be edited) [#840](https://github.com/GetStream/stream-chat-swiftui/pull/840)
+
# [4.78.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.78.0)
_April 24, 2025_
diff --git a/DemoAppSwiftUI/AppDelegate.swift b/DemoAppSwiftUI/AppDelegate.swift
index e0c1e5bf0..7cf4a97aa 100644
--- a/DemoAppSwiftUI/AppDelegate.swift
+++ b/DemoAppSwiftUI/AppDelegate.swift
@@ -63,7 +63,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
#endif
let utils = Utils(
+ channelListConfig: ChannelListConfig(
+ messageRelativeDateFormatEnabled: true
+ ),
messageListConfig: MessageListConfig(
+ messageDisplayOptions: .init(showOriginalTranslatedButton: true),
dateIndicatorPlacement: .messageList,
userBlockingEnabled: true,
bouncedMessagesAlertActionsEnabled: true,
diff --git a/Gemfile b/Gemfile
index 3953c7216..48cf60c63 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,7 +6,6 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
gem 'danger', group: :danger_dependencies
gem 'fastlane', group: :fastlane_dependencies
-gem 'jazzy'
gem 'json'
gem 'rubocop', '1.38', group: :rubocop_dependencies
gem 'sinatra', group: :sinatra_dependencies
@@ -18,7 +17,6 @@ group :fastlane_dependencies do
gem 'cocoapods'
gem 'fastlane-plugin-lizard'
gem 'plist'
- gem 'xcode-install'
gem 'xctest_list'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 2a789d4ae..c8e634605 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -5,8 +5,9 @@ GEM
base64
nkf
rexml
- activesupport (7.2.1)
+ activesupport (7.2.2.1)
base64
+ benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
@@ -22,37 +23,40 @@ GEM
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.17)
- ast (2.4.2)
+ ast (2.4.3)
atomos (0.1.3)
- aws-eventstream (1.3.0)
- aws-partitions (1.987.0)
- aws-sdk-core (3.209.1)
+ aws-eventstream (1.3.2)
+ aws-partitions (1.1091.0)
+ aws-sdk-core (3.222.2)
aws-eventstream (~> 1, >= 1.3.0)
- aws-partitions (~> 1, >= 1.651.0)
+ aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
+ base64
jmespath (~> 1, >= 1.6.1)
- aws-sdk-kms (1.94.0)
- aws-sdk-core (~> 3, >= 3.207.0)
+ logger
+ aws-sdk-kms (1.99.0)
+ aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
- aws-sdk-s3 (1.167.0)
- aws-sdk-core (~> 3, >= 3.207.0)
+ aws-sdk-s3 (1.183.0)
+ aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
- aws-sigv4 (1.10.0)
+ aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
- bigdecimal (3.1.8)
+ benchmark (0.4.0)
+ bigdecimal (3.1.9)
claide (1.1.0)
claide-plugins (0.9.2)
cork
nap
open4 (~> 1.3)
clamp (1.3.2)
- cocoapods (1.15.2)
+ cocoapods (1.16.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
- cocoapods-core (= 1.15.2)
+ cocoapods-core (= 1.16.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
@@ -66,8 +70,8 @@ GEM
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
- xcodeproj (>= 1.23.0, < 2.0)
- cocoapods-core (1.15.2)
+ xcodeproj (>= 1.27.0, < 2.0)
+ cocoapods-core (1.16.2)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
@@ -91,11 +95,12 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
- concurrent-ruby (1.3.4)
- connection_pool (2.4.1)
+ concurrent-ruby (1.3.5)
+ connection_pool (2.5.2)
cork (0.3.0)
colored2 (~> 3.1)
- danger (9.5.0)
+ danger (9.5.1)
+ base64 (~> 0.2)
claide (~> 1.0)
claide-plugins (>= 0.9.2)
colored2 (~> 3.1)
@@ -106,13 +111,14 @@ GEM
kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.0)
octokit (>= 4.0)
+ pstore (~> 0.1)
terminal-table (>= 1, < 4)
danger-commit_lint (0.0.7)
danger-plugin-api (~> 1.0)
danger-plugin-api (1.0.0)
danger (> 2.0)
declarative (0.0.20)
- digest-crc (0.6.5)
+ digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
@@ -143,8 +149,8 @@ GEM
faraday-http-cache (2.5.1)
faraday (>= 0.8)
faraday-httpclient (1.0.1)
- faraday-multipart (1.0.4)
- multipart-post (~> 2)
+ faraday-multipart (1.1.0)
+ multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
@@ -152,8 +158,8 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
- fastimage (2.3.1)
- fastlane (2.224.0)
+ fastimage (2.4.0)
+ fastlane (2.227.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -169,6 +175,7 @@ GEM
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
+ fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
@@ -192,7 +199,7 @@ GEM
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
- xcpretty (~> 0.3.0)
+ xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-create_xcframework (1.1.2)
fastlane-plugin-lizard (1.3.3)
@@ -200,10 +207,12 @@ GEM
fastlane
pry
fastlane-plugin-sonarcloud_metric_kit (0.2.1)
- fastlane-plugin-stream_actions (0.3.77)
+ fastlane-plugin-stream_actions (0.3.79)
xctest_list (= 1.2.1)
- fastlane-plugin-versioning (0.6.0)
- ffi (1.17.0)
+ fastlane-plugin-versioning (0.7.1)
+ fastlane-sirp (1.0.0)
+ sysrandom (~> 1.0)
+ ffi (1.17.2)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
@@ -226,12 +235,12 @@ GEM
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
- google-cloud-core (1.7.1)
+ google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
- google-cloud-errors (1.4.0)
+ google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
@@ -247,91 +256,82 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
- http-cookie (1.0.7)
+ http-cookie (1.0.8)
domain_name (~> 0.5)
- httpclient (2.8.3)
- i18n (1.14.6)
+ httpclient (2.9.0)
+ mutex_m
+ i18n (1.14.7)
concurrent-ruby (~> 1.0)
- jazzy (0.15.2)
- cocoapods (~> 1.5)
- mustache (~> 1.1)
- open4 (~> 1.3)
- redcarpet (~> 3.4)
- rexml (>= 3.2.7, < 4.0)
- rouge (>= 2.0.6, < 5.0)
- sassc (~> 2.1)
- sqlite3 (~> 1.3)
- xcinvoke (~> 0.3.0)
jmespath (1.6.2)
- json (2.7.2)
- jwt (2.9.3)
+ json (2.11.3)
+ jwt (2.10.1)
base64
- kramdown (2.4.0)
- rexml
+ kramdown (2.5.1)
+ rexml (>= 3.3.9)
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
- liferaft (0.0.6)
- logger (1.6.1)
+ logger (1.7.0)
method_source (1.1.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
mini_portile2 (2.8.8)
- minitest (5.25.1)
+ minitest (5.25.5)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.4.1)
- mustache (1.1.1)
mustermann (3.0.3)
ruby2_keywords (~> 0.0.1)
- nanaimo (0.3.0)
+ mutex_m (0.3.0)
+ nanaimo (0.4.0)
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
- nio4r (2.7.3)
+ nio4r (2.7.4)
nkf (0.2.0)
nokogiri (1.18.8)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
- octokit (9.1.0)
+ octokit (10.0.0)
faraday (>= 1, < 3)
sawyer (~> 0.9)
open4 (1.3.4)
- optparse (0.5.0)
+ optparse (0.6.0)
os (1.1.4)
- parallel (1.26.3)
- parser (3.3.5.0)
+ parallel (1.27.0)
+ parser (3.3.8.0)
ast (~> 2.4.1)
racc
- plist (3.7.1)
- pry (0.14.2)
+ plist (3.7.2)
+ prism (1.4.0)
+ pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
+ pstore (0.2.0)
public_suffix (4.0.7)
- puma (6.4.3)
+ puma (6.6.0)
nio4r (~> 2.0)
racc (1.8.1)
- rack (3.1.12)
- rack-protection (4.1.0)
+ rack (3.1.14)
+ rack-protection (4.1.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
rack (>= 3.0.0, < 4)
- rack-session (2.0.0)
+ rack-session (2.1.1)
+ base64 (>= 0.1.0)
rack (>= 3.0.0)
- rackup (2.1.0)
+ rackup (2.2.1)
rack (>= 3)
- webrick (~> 1.8)
rainbow (3.1.1)
rake (13.2.1)
- rchardet (1.8.0)
- redcarpet (3.6.0)
- regexp_parser (2.9.2)
+ rchardet (1.9.0)
+ regexp_parser (2.10.0)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
- rexml (3.3.9)
- rouge (2.0.7)
+ rexml (3.4.1)
+ rouge (3.28.0)
rubocop (1.38.0)
json (~> 2.3)
parallel (~> 1.10)
@@ -342,8 +342,9 @@ GEM
rubocop-ast (>= 1.23.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
- rubocop-ast (1.32.3)
- parser (>= 3.3.1.0)
+ rubocop-ast (1.44.1)
+ parser (>= 3.3.7.2)
+ prism (~> 1.4)
rubocop-performance (1.19.1)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
@@ -352,13 +353,11 @@ GEM
ruby-macho (2.5.1)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
- rubyzip (2.3.2)
- sassc (2.4.0)
- ffi (~> 1.9)
+ rubyzip (2.4.1)
sawyer (0.9.2)
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
- securerandom (0.3.1)
+ securerandom (0.4.1)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
@@ -368,25 +367,24 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
- sinatra (4.1.0)
+ sinatra (4.1.1)
logger (>= 1.6.0)
mustermann (~> 3.0)
rack (>= 3.0.0, < 4)
- rack-protection (= 4.1.0)
+ rack-protection (= 4.1.1)
rack-session (>= 2.0.0, < 3)
tilt (~> 2.0)
- slather (2.8.4)
+ slather (2.8.5)
CFPropertyList (>= 2.2, < 4)
activesupport
clamp (~> 1.3)
nokogiri (>= 1.14.3)
- xcodeproj (~> 1.25)
- sqlite3 (1.7.3)
- mini_portile2 (~> 2.8.0)
+ xcodeproj (~> 1.27)
+ sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
- tilt (2.4.0)
+ tilt (2.6.0)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
@@ -398,22 +396,16 @@ GEM
concurrent-ruby (~> 1.0)
uber (0.1.0)
unicode-display_width (2.6.0)
- webrick (1.8.2)
word_wrap (1.0.0)
- xcinvoke (0.3.0)
- liferaft (~> 0.0.6)
- xcode-install (2.8.1)
- claide (>= 0.9.1)
- fastlane (>= 2.1.0, < 3.0.0)
- xcodeproj (1.25.1)
+ xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
- nanaimo (~> 0.3.0)
+ nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
- xcpretty (0.3.0)
- rouge (~> 2.0.7)
+ xcpretty (0.4.1)
+ rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
xctest_list (1.2.1)
@@ -429,9 +421,8 @@ DEPENDENCIES
fastlane-plugin-create_xcframework
fastlane-plugin-lizard
fastlane-plugin-sonarcloud_metric_kit
- fastlane-plugin-stream_actions (= 0.3.77)
+ fastlane-plugin-stream_actions (= 0.3.79)
fastlane-plugin-versioning
- jazzy
json
plist
puma
@@ -441,7 +432,6 @@ DEPENDENCIES
rubocop-require_tools
sinatra
slather
- xcode-install
xctest_list
BUNDLED WITH
diff --git a/Package.swift b/Package.swift
index 85a2bacc4..304688be1 100644
--- a/Package.swift
+++ b/Package.swift
@@ -16,7 +16,7 @@ let package = Package(
)
],
dependencies: [
- .package(url: "https://github.com/GetStream/stream-chat-swift.git", from: "4.78.0"),
+ .package(url: "https://github.com/GetStream/stream-chat-swift.git", from: "4.79.0"),
],
targets: [
.target(
diff --git a/README.md b/README.md
index 856c56be3..c2ed386c3 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
## SwiftUI StreamChat SDK
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/AddUsersView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/AddUsersView.swift
index 71bbadc68..56fd27a38 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/AddUsersView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/AddUsersView.swift
@@ -65,7 +65,8 @@ struct AddUsersView: View {
id: user.id,
name: user.name ?? "",
imageURL: user.imageURL,
- size: CGSize(width: itemSize, height: itemSize)
+ size: CGSize(width: itemSize, height: itemSize),
+ extraData: user.extraData
)
factory.makeMessageAvatarView(for: userDisplayInfo)
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatChannelInfoHelperViews.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatChannelInfoHelperViews.swift
index 16c0e7978..2da62cdca 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatChannelInfoHelperViews.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatChannelInfoHelperViews.swift
@@ -264,7 +264,8 @@ struct ChatInfoDirectChannelView: View {
id: participant?.chatUser.id ?? "",
name: participant?.chatUser.name ?? "",
imageURL: participant?.chatUser.imageURL,
- size: .init(width: 64, height: 64)
+ size: .init(width: 64, height: 64),
+ extraData: participant?.chatUser.extraData ?? [:]
)
factory.makeMessageAvatarView(for: displayInfo)
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatInfoParticipantsView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatInfoParticipantsView.swift
index 9b8e9e150..2e2f5519c 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatInfoParticipantsView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatInfoParticipantsView.swift
@@ -32,7 +32,8 @@ struct ChatInfoParticipantsView: View {
let displayInfo = UserDisplayInfo(
id: participant.chatUser.id,
name: participant.chatUser.name ?? "",
- imageURL: participant.chatUser.imageURL
+ imageURL: participant.chatUser.imageURL,
+ extraData: participant.chatUser.extraData
)
factory.makeMessageAvatarView(for: displayInfo)
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift
index 9cc09d28e..30f6e993a 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift
@@ -83,7 +83,8 @@ public struct MediaAttachmentsView: View {
id: mediaItem.message.author.id,
name: mediaItem.message.author.name ?? "",
imageURL: mediaItem.message.author.imageURL,
- size: .init(width: 24, height: 24)
+ size: .init(width: 24, height: 24),
+ extraData: mediaItem.message.author.extraData
)
)
.overlay(
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/PinnedMessagesView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/PinnedMessagesView.swift
index a40b08780..d3af0ccba 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/PinnedMessagesView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/PinnedMessagesView.swift
@@ -71,7 +71,8 @@ struct PinnedMessageView: View {
id: message.author.id,
name: message.author.name ?? "",
imageURL: message.author.imageURL,
- size: avatarSize
+ size: avatarSize,
+ extraData: message.author.extraData
)
)
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift
index b6e5c90bc..9b1329196 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift
@@ -130,7 +130,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
}
@Published public private(set) var channel: ChatChannel?
-
+
public var isMessageThread: Bool {
messageController != nil
}
@@ -158,7 +158,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
channelDataSource.delegate = self
messages = channelDataSource.messages
channel = channelController.channel
-
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
if let scrollToMessage, let parentMessageId = scrollToMessage.parentMessageId, messageController == nil {
let message = channelController.dataStore.message(id: parentMessageId)
@@ -221,7 +221,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
checkHeaderType()
checkUnreadCount()
}
-
+
@objc
private func selectedMessageThread(notification: Notification) {
if let message = notification.userInfo?[MessageRepliesConstants.selectedMessage] as? ChatMessage {
@@ -490,7 +490,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
messageActionExecuted(.init(message: message, identifier: "edit"))
}
- public func messageActionExecuted(_ messageActionInfo: MessageActionInfo) {
+ open func messageActionExecuted(_ messageActionInfo: MessageActionInfo) {
utils.messageActionsResolver.resolveMessageAction(
info: messageActionInfo,
viewModel: self
@@ -498,6 +498,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
}
@objc public func onViewAppear() {
+ utils.originalTranslationsStore.clear()
setActive()
messages = channelDataSource.messages
firstUnreadMessageId = channelDataSource.firstUnreadMessageId
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift
index ce5416521..f2b5e67f8 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift
@@ -453,6 +453,13 @@ open class MessageComposerViewModel: ObservableObject {
}
public var showCommandsOverlay: Bool {
+ // Mentions are really not commands, but at the moment this flag controls
+ // if the mentions are displayed or not, so if the command is related to mentions
+ // then we need to ignore if commands are available or not.
+ let isMentionsSuggestions = composerCommand?.id == "mentions"
+ if isMentionsSuggestions {
+ return true
+ }
let commandAvailable = composerCommand != nil
let configuredCommandsAvailable = channelController.channel?.config.commands.count ?? 0 > 0
return commandAvailable && configuredCommandsAvailable
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift
index 6681b401d..7285fe704 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift
@@ -7,7 +7,7 @@ import StreamChat
import SwiftUI
/// View used for displaying image attachments in a gallery.
-public struct GalleryView: View {
+public struct GalleryView: View {
@Environment(\.presentationMode) var presentationMode
@@ -15,6 +15,7 @@ public struct GalleryView: View {
@Injected(\.fonts) private var fonts
@Injected(\.images) private var images
+ private let viewFactory: Factory
var mediaAttachments: [MediaAttachment]
var author: ChatUser
@Binding var isShown: Bool
@@ -23,6 +24,7 @@ public struct GalleryView: View {
@State private var gridShown = false
public init(
+ viewFactory: Factory = DefaultViewFactory.shared,
imageAttachments: [ChatMessageImageAttachment],
author: ChatUser,
isShown: Binding,
@@ -30,6 +32,7 @@ public struct GalleryView: View {
) {
let mediaAttachments = imageAttachments.map { MediaAttachment(from: $0) }
self.init(
+ viewFactory: viewFactory,
mediaAttachments: mediaAttachments,
author: author,
isShown: isShown,
@@ -38,11 +41,13 @@ public struct GalleryView: View {
}
public init(
+ viewFactory: Factory = DefaultViewFactory.shared,
mediaAttachments: [MediaAttachment],
author: ChatUser,
isShown: Binding,
selected: Int
) {
+ self.viewFactory = viewFactory
self.mediaAttachments = mediaAttachments
self.author = author
_isShown = isShown
@@ -52,10 +57,10 @@ public struct GalleryView: View {
public var body: some View {
GeometryReader { reader in
VStack {
- GalleryHeaderView(
+ viewFactory.makeGalleryHeaderView(
title: author.name ?? "",
subtitle: author.onlineText,
- isShown: $isShown
+ shown: $isShown
)
TabView(selection: $selected) {
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/VideoPlayerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/VideoPlayerView.swift
index 2a3255c33..2e6d35da1 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/VideoPlayerView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/VideoPlayerView.swift
@@ -7,7 +7,7 @@ import StreamChat
import SwiftUI
/// View used for displaying videos.
-public struct VideoPlayerView: View {
+public struct VideoPlayerView: View {
@Environment(\.presentationMode) var presentationMode
@Injected(\.fonts) private var fonts
@@ -18,6 +18,7 @@ public struct VideoPlayerView: View {
utils.fileCDN
}
+ private let viewFactory: Factory
let attachment: ChatMessageVideoAttachment
let author: ChatUser
@Binding var isShown: Bool
@@ -26,10 +27,12 @@ public struct VideoPlayerView: View {
@State private var error: Error?
public init(
+ viewFactory: Factory = DefaultViewFactory.shared,
attachment: ChatMessageVideoAttachment,
author: ChatUser,
isShown: Binding
) {
+ self.viewFactory = viewFactory
self.attachment = attachment
self.author = author
_isShown = isShown
@@ -37,10 +40,10 @@ public struct VideoPlayerView: View {
public var body: some View {
VStack {
- GalleryHeaderView(
+ viewFactory.makeVideoPlayerHeaderView(
title: author.name ?? "",
subtitle: author.onlineText,
- isShown: $isShown
+ shown: $isShown
)
if let avPlayer {
VideoPlayer(player: avPlayer)
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift
index eeecbc7f7..b86521340 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift
@@ -7,6 +7,7 @@ import StreamChat
import SwiftUI
public struct MessageContainerView: View {
+ @StateObject var messageViewModel: MessageViewModel
@Environment(\.channelTranslationLanguage) var translationLanguage
@Injected(\.fonts) private var fonts
@@ -33,10 +34,12 @@ public struct MessageContainerView: View {
@GestureState private var offset: CGSize = .zero
private let replyThreshold: CGFloat = 60
- private let paddingValue: CGFloat = 8
-
- var isSwipeToReplyPossible: Bool {
- message.isInteractionEnabled && channel.config.repliesEnabled
+ private var paddingValue: CGFloat {
+ utils.messageListConfig.messagePaddings.singleBottom
+ }
+
+ private var groupMessageInterItemSpacing: CGFloat {
+ utils.messageListConfig.messagePaddings.groupBottom
}
public init(
@@ -49,7 +52,8 @@ public struct MessageContainerView: View {
isLast: Bool,
scrolledId: Binding,
quotedMessage: Binding,
- onLongPress: @escaping (MessageDisplayInfo) -> Void
+ onLongPress: @escaping (MessageDisplayInfo) -> Void,
+ viewModel: MessageViewModel? = nil
) {
self.factory = factory
self.channel = channel
@@ -59,21 +63,27 @@ public struct MessageContainerView: View {
self.isInThread = isInThread
self.isLast = isLast
self.onLongPress = onLongPress
+ _messageViewModel = .init(
+ wrappedValue: viewModel ?? MessageViewModel(
+ message: message,
+ channel: channel
+ )
+ )
_scrolledId = scrolledId
_quotedMessage = quotedMessage
}
public var body: some View {
HStack(alignment: .bottom) {
- if message.type == .system || (message.type == .error && message.isBounced == false) {
+ if messageViewModel.systemMessageShown {
factory.makeSystemMessageView(message: message)
} else {
- if message.isRightAligned {
+ if messageViewModel.isRightAligned {
MessageSpacer(spacerWidth: spacerWidth)
} else {
- if messageListConfig.messageDisplayOptions.showAvatars(for: channel) {
+ if let userDisplayInfo = messageViewModel.userDisplayInfo {
factory.makeMessageAvatarView(
- for: message.authorDisplayInfo
+ for: userDisplayInfo
)
.opacity(showsAllInfo ? 1 : 0)
.offset(y: bottomReactionsShown ? offsetYAvatar : 0)
@@ -81,8 +91,8 @@ public struct MessageContainerView: View {
}
}
- VStack(alignment: message.isRightAligned ? .trailing : .leading) {
- if isMessagePinned {
+ VStack(alignment: messageViewModel.isRightAligned ? .trailing : .leading) {
+ if messageViewModel.isPinned {
MessagePinDetailsView(
message: message,
reactionsShown: topReactionsShown
@@ -109,9 +119,7 @@ public struct MessageContainerView: View {
}
)
: nil
-
- ((message.localState == .sendingFailed || message.isBounced) && !message.text.isEmpty) ?
- SendFailureIndicator() : nil
+ messageViewModel.failureIndicatorShown ? SendFailureIndicator() : nil
}
)
.background(
@@ -137,7 +145,7 @@ public struct MessageContainerView: View {
coordinateSpace: .local
)
.updating($offset) { (value, gestureState, _) in
- guard isSwipeToReplyPossible else {
+ guard messageViewModel.isSwipeToQuoteReplyPossible else {
return
}
// Using updating since onEnded is not called if the gesture is canceled.
@@ -229,12 +237,12 @@ public struct MessageContainerView: View {
}
}
- if message.textContent(for: translationLanguage) != nil,
- let localizedName = translationLanguage?.localizedName {
- Text(L10n.Message.translatedTo(localizedName))
- .font(fonts.footnote)
- .foregroundColor(Color(colors.subtitleText))
+ if messageViewModel.translatedText != nil {
+ factory.makeMessageTranslationFooterView(
+ messageViewModel: messageViewModel
+ )
}
+
if showsAllInfo && !message.isDeleted {
if message.isSentByCurrentUser && channel.config.readEventsEnabled {
HStack(spacing: 4) {
@@ -243,15 +251,13 @@ public struct MessageContainerView: View {
message: message
)
- if messageListConfig.messageDisplayOptions.showMessageDate {
+ if messageViewModel.messageDateShown {
factory.makeMessageDateView(for: message)
}
}
- } else if !message.isRightAligned
- && channel.memberCount > 2
- && messageListConfig.messageDisplayOptions.showAuthorName {
+ } else if messageViewModel.authorAndDateShown {
factory.makeMessageAuthorAndDateView(for: message)
- } else if messageListConfig.messageDisplayOptions.showMessageDate {
+ } else if messageViewModel.messageDateShown {
factory.makeMessageDateView(for: message)
}
}
@@ -265,20 +271,21 @@ public struct MessageContainerView: View {
: nil
)
- if !message.isRightAligned {
+ if !messageViewModel.isRightAligned {
MessageSpacer(spacerWidth: spacerWidth)
}
}
}
.padding(
.top,
- topReactionsShown && !isMessagePinned ? messageListConfig.messageDisplayOptions.reactionsTopPadding(message) : 0
+ topReactionsShown && !messageViewModel.isPinned ? messageListConfig.messageDisplayOptions
+ .reactionsTopPadding(message) : 0
)
.padding(.horizontal, messageListConfig.messagePaddings.horizontal)
- .padding(.bottom, showsAllInfo || isMessagePinned ? paddingValue : 2)
+ .padding(.bottom, showsAllInfo || messageViewModel.isPinned ? paddingValue : groupMessageInterItemSpacing)
.padding(.top, isLast ? paddingValue : 0)
- .background(isMessagePinned ? Color(colors.pinnedBackground) : nil)
- .padding(.bottom, isMessagePinned ? paddingValue / 2 : 0)
+ .background(messageViewModel.isPinned ? Color(colors.pinnedBackground) : nil)
+ .padding(.bottom, messageViewModel.isPinned ? paddingValue / 2 : 0)
.transition(
message.isSentByCurrentUser ?
messageListConfig.messageDisplayOptions.currentUserMessageTransition :
@@ -286,22 +293,23 @@ public struct MessageContainerView: View {
)
.accessibilityElement(children: .contain)
.accessibilityIdentifier("MessageContainerView")
+ // This is needed for the LinkDetectionTextView to work properly.
+ // TODO: This should be refactored on v5 so the TextView does not depend directly on the view model.
+ .environment(\.messageViewModel, messageViewModel)
+ .onChange(of: message, perform: { message in messageViewModel.message = message })
+ .onChange(of: channel) { channel in messageViewModel.channel = channel }
}
private var maximumHorizontalSwipeDisplacement: CGFloat {
replyThreshold + 30
}
- private var isMessagePinned: Bool {
- message.pinDetails != nil
- }
-
private var contentWidth: CGFloat {
let padding: CGFloat = messageListConfig.messagePaddings.horizontal
let minimumWidth: CGFloat = 240
let available = max(minimumWidth, (width ?? 0) - spacerWidth) - 2 * padding
let avatarSize: CGFloat = CGSize.messageAvatarSize.width + padding
- let totalWidth = message.isRightAligned ? available : available - avatarSize
+ let totalWidth = messageViewModel.isRightAligned ? available : available - avatarSize
return totalWidth
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift
index e9fa2fe23..3604093e1 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift
@@ -110,13 +110,19 @@ public struct MessagePaddings {
/// Horizontal padding for messages.
public let horizontal: CGFloat
public let quotedViewPadding: CGFloat
+ public let singleBottom: CGFloat
+ public let groupBottom: CGFloat
public init(
horizontal: CGFloat = 8,
- quotedViewPadding: CGFloat = 8
+ quotedViewPadding: CGFloat = 8,
+ singleBottom: CGFloat = 8,
+ groupBottom: CGFloat = 2
) {
self.horizontal = horizontal
self.quotedViewPadding = quotedViewPadding
+ self.singleBottom = singleBottom
+ self.groupBottom = groupBottom
}
}
@@ -142,6 +148,7 @@ public struct MessageDisplayOptions {
public let otherUserMessageTransition: AnyTransition
public let shouldAnimateReactions: Bool
public let reactionsPlacement: ReactionsPlacement
+ public let showOriginalTranslatedButton: Bool
public let messageLinkDisplayResolver: (ChatMessage) -> [NSAttributedString.Key: Any]
public let spacerWidth: (CGFloat) -> CGFloat
public let reactionsTopPadding: (ChatMessage) -> CGFloat
@@ -161,6 +168,7 @@ public struct MessageDisplayOptions {
otherUserMessageTransition: AnyTransition = .identity,
shouldAnimateReactions: Bool = true,
reactionsPlacement: ReactionsPlacement = .top,
+ showOriginalTranslatedButton: Bool = false,
messageLinkDisplayResolver: @escaping (ChatMessage) -> [NSAttributedString.Key: Any] = MessageDisplayOptions
.defaultLinkDisplay,
spacerWidth: @escaping (CGFloat) -> CGFloat = MessageDisplayOptions.defaultSpacerWidth,
@@ -184,6 +192,7 @@ public struct MessageDisplayOptions {
self.newMessagesSeparatorSize = newMessagesSeparatorSize
self.dateSeparator = dateSeparator
self.reactionsPlacement = reactionsPlacement
+ self.showOriginalTranslatedButton = showOriginalTranslatedButton
}
public func showAvatars(for channel: ChatChannel) -> Bool {
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListHelperViews.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListHelperViews.swift
index 03f6a8d99..79f3907ce 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListHelperViews.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListHelperViews.swift
@@ -112,7 +112,7 @@ public struct MessageReadIndicatorView: View {
public var body: some View {
HStack(spacing: 2) {
- if showReadCount && !readUsers.isEmpty {
+ if showReadCount && shouldShowReads {
Text("\(readUsers.count)")
.font(fonts.footnoteBold)
.foregroundColor(colors.tintColor)
@@ -122,9 +122,9 @@ public struct MessageReadIndicatorView: View {
uiImage: image
)
.customizable()
- .foregroundColor(!readUsers.isEmpty ? colors.tintColor : Color(colors.textLowEmphasis))
+ .foregroundColor(shouldShowReads ? colors.tintColor : Color(colors.textLowEmphasis))
.frame(height: 16)
- .opacity(localState == .sendingFailed ? 0.0 : 1)
+ .opacity(localState == .sendingFailed || localState == .syncingFailed ? 0.0 : 1)
.accessibilityLabel(
Text(
readUsers.isEmpty ? L10n.Message.ReadStatus.seenByNoOne : L10n.Message.ReadStatus.seenByOthers
@@ -137,12 +137,15 @@ public struct MessageReadIndicatorView: View {
}
private var image: UIImage {
- !readUsers.isEmpty ? images.readByAll : (isMessageSending ? images.messageReceiptSending : images.messageSent)
+ shouldShowReads ? images.readByAll : (isMessageSending ? images.messageReceiptSending : images.messageSent)
}
private var isMessageSending: Bool {
- localState == .sending
- || localState == .pendingSend
+ localState == .sending || localState == .pendingSend || localState == .syncing
+ }
+
+ private var shouldShowReads: Bool {
+ !readUsers.isEmpty && !isMessageSending
}
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift
index e30209027..8e52f5313 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift
@@ -6,7 +6,6 @@ import StreamChat
import SwiftUI
public struct MessageListView: View, KeyboardReadable {
-
@Injected(\.utils) private var utils
@Injected(\.chatClient) private var chatClient
@Injected(\.colors) private var colors
@@ -603,6 +602,10 @@ private struct ChannelTranslationLanguageKey: EnvironmentKey {
static let defaultValue: TranslationLanguage? = nil
}
+private struct MessageViewModelKey: EnvironmentKey {
+ static let defaultValue: MessageViewModel? = nil
+}
+
extension EnvironmentValues {
var channelTranslationLanguage: TranslationLanguage? {
get {
@@ -612,4 +615,13 @@ extension EnvironmentValues {
self[ChannelTranslationLanguageKey.self] = newValue
}
}
+
+ var messageViewModel: MessageViewModel? {
+ get {
+ self[MessageViewModelKey.self]
+ }
+ set {
+ self[MessageViewModelKey.self] = newValue
+ }
+ }
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageTranslationFooterView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageTranslationFooterView.swift
new file mode 100644
index 000000000..9ee2cf15f
--- /dev/null
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageTranslationFooterView.swift
@@ -0,0 +1,63 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import StreamChat
+import SwiftUI
+
+public struct MessageTranslationFooterView: View {
+ @ObservedObject var messageViewModel: MessageViewModel
+
+ @Injected(\.fonts) private var fonts
+ @Injected(\.colors) private var colors
+ @Injected(\.utils) private var utils
+
+ public init(
+ messageViewModel: MessageViewModel
+ ) {
+ self.messageViewModel = messageViewModel
+ }
+
+ public var body: some View {
+ if utils.messageListConfig.messageDisplayOptions.showOriginalTranslatedButton {
+ HStack(spacing: 4) {
+ if !messageViewModel.originalTextShown {
+ translatedToView
+ separatorView
+ }
+ showOriginalButton
+ }
+ } else {
+ translatedToView
+ }
+ }
+
+ private var translatedToView: some View {
+ Text(messageViewModel.translatedLanguageText ?? "")
+ .font(fonts.footnote)
+ .foregroundColor(Color(colors.subtitleText))
+ }
+
+ private var separatorView: some View {
+ Text("•")
+ .font(fonts.footnote)
+ .foregroundColor(Color(colors.subtitleText))
+ }
+
+ private var showOriginalButton: some View {
+ Button(
+ action: {
+ if messageViewModel.originalTextShown {
+ messageViewModel.hideOriginalText()
+ } else {
+ messageViewModel.showOriginalText()
+ }
+ },
+ label: {
+ Text(messageViewModel.originalTranslationButtonText)
+ .font(fonts.footnote)
+ .foregroundColor(Color(colors.subtitleText))
+ }
+ )
+ }
+}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift
index 8883c4867..30dbad471 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift
@@ -252,53 +252,45 @@ struct StreamTextView: View {
public struct LinkDetectionTextView: View {
@Environment(\.layoutDirection) var layoutDirection
@Environment(\.channelTranslationLanguage) var translationLanguage
-
+ @Environment(\.messageViewModel) var messageViewModel
+
@Injected(\.colors) var colors
@Injected(\.fonts) var fonts
@Injected(\.utils) var utils
var message: ChatMessage
-
- var text: LocalizedStringKey {
- LocalizedStringKey(message.adjustedText)
- }
-
- @State var displayedText: AttributedString?
-
+
+ // The translations store is used to detect changes so the textContent is re-rendered.
+ // The @Environment(\.messageViewModel) is not reactive like @EnvironmentObject.
+ // TODO: On v5 the TextView should be refactored and not depend directly on the view model.
+ @ObservedObject var originalTranslationsStore = InjectedValues[\.utils].originalTranslationsStore
+
+ @State var text: AttributedString?
@State var linkDetector = TextLinkDetector()
-
@State var tintColor = InjectedValues[\.colors].tintColor
- public init(message: ChatMessage) {
+ public init(
+ message: ChatMessage
+ ) {
self.message = message
}
public var body: some View {
Group {
- if let displayedText {
- Text(displayedText)
- } else {
- Text(message.adjustedText)
- }
+ Text(text ?? displayText)
}
.foregroundColor(textColor(for: message))
.font(fonts.body)
.tint(tintColor)
- .onAppear {
- displayedText = attributedString(for: message)
+ .onChange(of: message) { message in
+ messageViewModel?.message = message
+ text = displayText
}
- .onChange(of: message, perform: { updated in
- displayedText = attributedString(for: updated)
- })
}
- private func attributedString(for message: ChatMessage) -> AttributedString {
- var text = message.adjustedText
-
- // Translation
- if let translatedText = message.textContent(for: translationLanguage) {
- text = translatedText
- }
+ var displayText: AttributedString {
+ let text = messageViewModel?.textContent ?? message.text
+
// Markdown
let attributes = AttributeContainer()
.foregroundColor(textColor(for: message))
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageViewModel.swift
new file mode 100644
index 000000000..d9c22fcd3
--- /dev/null
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageViewModel.swift
@@ -0,0 +1,151 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Combine
+import StreamChat
+
+/// The view model that contains the logic for displaying a message in the message list view.
+open class MessageViewModel: ObservableObject {
+ @Injected(\.utils) private var utils
+ @Injected(\.chatClient) private var chatClient
+
+ @Published public internal(set) var message: ChatMessage
+ @Published public internal(set) var channel: ChatChannel?
+ private var cancellables = Set()
+
+ public init(
+ message: ChatMessage,
+ channel: ChatChannel?
+ ) {
+ self.message = message
+ self.channel = channel
+ utils.originalTranslationsStore.$originalTextMessageIds.sink(
+ receiveValue: { [weak self] _ in
+ self?.objectWillChange.send()
+ }
+ )
+ .store(in: &cancellables)
+ }
+
+ // MARK: - Inputs
+
+ /// Show the original text of the message.
+ public func showOriginalText() {
+ utils.originalTranslationsStore.showOriginalText(for: message.id)
+ }
+
+ /// Hide the original text of the message to show the translated text.
+ public func hideOriginalText() {
+ utils.originalTranslationsStore.hideOriginalText(for: message.id)
+ }
+
+ // MARK: - Outputs
+
+ public var originalTextShown: Bool {
+ utils.originalTranslationsStore.shouldShowOriginalText(for: message.id)
+ }
+
+ public var systemMessageShown: Bool {
+ message.type == .system || (message.type == .error && message.isBounced == false)
+ }
+
+ public var reactionsShown: Bool {
+ !message.reactionScores.isEmpty
+ && !message.isDeleted
+ && channel?.config.reactionsEnabled == true
+ }
+
+ public var failureIndicatorShown: Bool {
+ message.isLastActionFailed && !message.text.isEmpty
+ }
+
+ open var authorAndDateShown: Bool {
+ guard let channel = channel else { return false }
+ return !message.isRightAligned
+ && channel.memberCount > 2
+ && messageListConfig.messageDisplayOptions.showAuthorName
+ }
+
+ open var messageDateShown: Bool {
+ messageListConfig.messageDisplayOptions.showMessageDate
+ }
+
+ public var isPinned: Bool {
+ message.isPinned
+ }
+
+ public var isRightAligned: Bool {
+ message.isRightAligned
+ }
+
+ public var userDisplayInfo: UserDisplayInfo? {
+ guard let channel = channel else { return nil }
+ guard messageListConfig.messageDisplayOptions.showAvatars(for: channel) else { return nil }
+ return message.authorDisplayInfo
+ }
+
+ open var isSwipeToQuoteReplyPossible: Bool {
+ message.isInteractionEnabled && channel?.config.repliesEnabled == true
+ }
+
+ open var textContent: String {
+ if !originalTextShown, let translatedText = translatedText {
+ return translatedText
+ }
+
+ return message.adjustedText
+ }
+
+ public var translatedText: String? {
+ if let language = channel?.membership?.language,
+ let translatedText = message.textContent(for: language) {
+ return translatedText
+ }
+
+ return nil
+ }
+
+ public var translatedLanguageText: String? {
+ guard let localizedName = channel?.membership?.language?.localizedName else {
+ return nil
+ }
+
+ return L10n.Message.translatedTo(localizedName)
+ }
+
+ public var originalTranslationButtonText: String {
+ originalTextShown ? L10n.Message.showTranslation : L10n.Message.showOriginal
+ }
+
+ // MARK: - Helpers
+
+ private var messageListConfig: MessageListConfig {
+ utils.messageListConfig
+ }
+}
+
+/// A singleton store that keeps track of which messages have their original text shown.
+///
+/// **Note:** This is not thread-safe, it should only be used on the main thread.
+public class MessageOriginalTranslationsStore: ObservableObject {
+ internal init() {}
+
+ @Published var originalTextMessageIds: Set = []
+
+ public func shouldShowOriginalText(for messageId: MessageId) -> Bool {
+ originalTextMessageIds.contains(messageId)
+ }
+
+ public func showOriginalText(for messageId: MessageId) {
+ originalTextMessageIds.insert(messageId)
+ }
+
+ public func hideOriginalText(for messageId: MessageId) {
+ originalTextMessageIds.remove(messageId)
+ }
+
+ public func clear() {
+ originalTextMessageIds.removeAll()
+ }
+}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAttachmentView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAttachmentView.swift
index caaacda7b..3d796dbba 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAttachmentView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAttachmentView.swift
@@ -228,7 +228,8 @@ struct PollOptionView: View {
id: vote.user?.id ?? "",
name: vote.user?.name ?? "",
imageURL: vote.user?.imageURL,
- size: .init(width: 20, height: 20)
+ size: .init(width: 20, height: 20),
+ extraData: vote.user?.extraData ?? [:]
)
)
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollCommentsView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollCommentsView.swift
index c404d7ea5..495396ae5 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollCommentsView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollCommentsView.swift
@@ -43,7 +43,8 @@ struct PollCommentsView: View {
let displayInfo = UserDisplayInfo(
id: comment.user?.id ?? "",
name: comment.user?.name ?? "",
- imageURL: comment.user?.imageURL
+ imageURL: comment.user?.imageURL,
+ extraData: comment.user?.extraData ?? [:]
)
factory.makeMessageAvatarView(for: displayInfo)
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollResultsView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollResultsView.swift
index d6a4d5bf5..4d443da76 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollResultsView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollResultsView.swift
@@ -113,7 +113,8 @@ struct PollOptionResultsView: View {
id: vote.user?.id ?? "",
name: vote.user?.name ?? "",
imageURL: vote.user?.imageURL,
- size: .init(width: 20, height: 20)
+ size: .init(width: 20, height: 20),
+ extraData: vote.user?.extraData ?? [:]
)
)
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift
index 8dc7cc287..2d9c49373 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift
@@ -131,7 +131,7 @@ public extension MessageAction {
)
messageActions.append(markThreadUnreadAction)
}
- } else if !message.isSentByCurrentUser {
+ } else if !message.isSentByCurrentUser && channel.canReceiveReadEvents {
if !message.isPartOfThread || message.showReplyInChannel {
let markUnreadAction = markAsUnreadAction(
for: message,
@@ -145,8 +145,8 @@ public extension MessageAction {
}
}
- if message.isSentByCurrentUser {
- if message.poll == nil && message.giphyAttachments.isEmpty {
+ if message.poll == nil, message.giphyAttachments.isEmpty {
+ if channel.canUpdateAnyMessage || channel.canUpdateOwnMessage && message.isSentByCurrentUser {
let editAction = editMessageAction(
for: message,
channel: channel,
@@ -154,7 +154,9 @@ public extension MessageAction {
)
messageActions.append(editAction)
}
-
+ }
+
+ if message.isSentByCurrentUser {
let deleteAction = deleteMessageAction(
for: message,
channel: channel,
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsOverlayView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsOverlayView.swift
index 7d20b5e1a..a41e32d80 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsOverlayView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsOverlayView.swift
@@ -10,6 +10,7 @@ public struct ReactionsOverlayView: View {
@Injected(\.colors) private var colors
@StateObject var viewModel: ReactionsOverlayViewModel
+ @StateObject var messageViewModel: MessageViewModel
@State private var popIn = false
@State private var willPopOut = false
@@ -43,13 +44,21 @@ public struct ReactionsOverlayView: View {
minOriginY: CGFloat = 100,
bottomOffset: CGFloat = 0,
onBackgroundTap: @escaping () -> Void,
- onActionExecuted: @escaping (MessageActionInfo) -> Void
+ onActionExecuted: @escaping (MessageActionInfo) -> Void,
+ viewModel: ReactionsOverlayViewModel? = nil,
+ messageViewModel: MessageViewModel? = nil
) {
_viewModel = StateObject(
- wrappedValue: ViewModelsFactory.makeReactionsOverlayViewModel(
+ wrappedValue: viewModel ?? ViewModelsFactory.makeReactionsOverlayViewModel(
message: messageDisplayInfo.message
)
)
+ _messageViewModel = StateObject(
+ wrappedValue: messageViewModel ?? MessageViewModel(
+ message: messageDisplayInfo.message,
+ channel: channel
+ )
+ )
self.channel = channel
self.factory = factory
self.currentSnapshot = currentSnapshot
@@ -225,6 +234,9 @@ public struct ReactionsOverlayView: View {
isFirst: messageDisplayInfo.isFirst,
scrolledId: .constant(nil)
)
+ // This is needed for the LinkDetectionTextView to work properly.
+ // TODO: This should be refactored on v5 so the TextView does not depend directly on the view model.
+ .environment(\.messageViewModel, messageViewModel)
}
private func dismissReactionsOverlay(completion: @escaping () -> Void) {
diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChannelListConfig.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChannelListConfig.swift
new file mode 100644
index 000000000..6bd0aa495
--- /dev/null
+++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChannelListConfig.swift
@@ -0,0 +1,17 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+/// A configuration for channel lists.
+public struct ChannelListConfig {
+ public init(messageRelativeDateFormatEnabled: Bool = false) {
+ self.messageRelativeDateFormatEnabled = messageRelativeDateFormatEnabled
+ }
+
+ /// If true, the timestamp format depends on the time passed.
+ ///
+ /// Different date formats are used for today, yesterday, last 7 days, and older dates.
+ public var messageRelativeDateFormatEnabled: Bool
+}
diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift
index e36f5de9f..cd14fe253 100644
--- a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift
@@ -337,7 +337,11 @@ extension ChatChannel {
public var timestampText: String {
if let lastMessageAt = lastMessageAt {
- return InjectedValues[\.utils].dateFormatter.string(from: lastMessageAt)
+ let utils = InjectedValues[\.utils]
+ let formatter = utils.channelListConfig.messageRelativeDateFormatEnabled ?
+ utils.messageRelativeDateFormatter :
+ utils.dateFormatter
+ return formatter.string(from: lastMessageAt)
} else {
return ""
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/SearchResultsView.swift b/Sources/StreamChatSwiftUI/ChatChannelList/SearchResultsView.swift
index 455f2d8d0..ab4e6750a 100644
--- a/Sources/StreamChatSwiftUI/ChatChannelList/SearchResultsView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannelList/SearchResultsView.swift
@@ -158,7 +158,10 @@ struct SearchResultItem: View {
private var timestampText: String {
if let lastMessageAt = searchResult.channel.lastMessageAt {
- return utils.dateFormatter.string(from: lastMessageAt)
+ let formatter = utils.channelListConfig.messageRelativeDateFormatEnabled ?
+ utils.messageRelativeDateFormatter :
+ utils.dateFormatter
+ return formatter.string(from: lastMessageAt)
} else {
return ""
}
diff --git a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift
index 3e14a3829..514f2fa73 100644
--- a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift
+++ b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift
@@ -370,7 +370,15 @@ extension ViewFactory {
public func makeLastInGroupHeaderView(for message: ChatMessage) -> some View {
EmptyView()
}
-
+
+ public func makeMessageTranslationFooterView(
+ messageViewModel: MessageViewModel
+ ) -> some View {
+ MessageTranslationFooterView(
+ messageViewModel: messageViewModel
+ )
+ }
+
public func makeImageAttachmentView(
for message: ChatMessage,
isFirst: Bool,
@@ -452,6 +460,7 @@ extension ViewFactory {
options: MediaViewsOptions
) -> some View {
GalleryView(
+ viewFactory: self,
mediaAttachments: mediaAttachments,
author: message.author,
isShown: isShown,
@@ -459,6 +468,14 @@ extension ViewFactory {
)
}
+ public func makeGalleryHeaderView(
+ title: String,
+ subtitle: String,
+ shown: Binding
+ ) -> some View {
+ GalleryHeaderView(title: title, subtitle: subtitle, isShown: shown)
+ }
+
public func makeVideoPlayerView(
attachment: ChatMessageVideoAttachment,
message: ChatMessage,
@@ -466,12 +483,21 @@ extension ViewFactory {
options: MediaViewsOptions
) -> some View {
VideoPlayerView(
+ viewFactory: self,
attachment: attachment,
author: message.author,
isShown: isShown
)
}
+ public func makeVideoPlayerHeaderView(
+ title: String,
+ subtitle: String,
+ shown: Binding
+ ) -> some View {
+ GalleryHeaderView(title: title, subtitle: subtitle, isShown: shown)
+ }
+
public func makeDeletedMessageView(
for message: ChatMessage,
isFirst: Bool,
@@ -986,7 +1012,7 @@ extension ViewFactory {
currentUserId: chatClient.currentUserId,
message: message
)
- let showReadCount = channel.memberCount > 2
+ let showReadCount = channel.memberCount > 2 && !message.isLastActionFailed
return MessageReadIndicatorView(
readUsers: readUsers,
showReadCount: showReadCount,
diff --git a/Sources/StreamChatSwiftUI/Generated/L10n.swift b/Sources/StreamChatSwiftUI/Generated/L10n.swift
index 3d1f1baa9..109848843 100644
--- a/Sources/StreamChatSwiftUI/Generated/L10n.swift
+++ b/Sources/StreamChatSwiftUI/Generated/L10n.swift
@@ -352,6 +352,10 @@ internal enum L10n {
internal static var deletedMessagePlaceholder: String { L10n.tr("Localizable", "message.deleted-message-placeholder") }
/// Only visible to you
internal static var onlyVisibleToYou: String { L10n.tr("Localizable", "message.only-visible-to-you") }
+ /// Show Original
+ internal static var showOriginal: String { L10n.tr("Localizable", "message.showOriginal") }
+ /// Show Translation
+ internal static var showTranslation: String { L10n.tr("Localizable", "message.showTranslation") }
/// Translated to %@
internal static func translatedTo(_ p1: Any) -> String {
return L10n.tr("Localizable", "message.translatedTo", String(describing: p1))
diff --git a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift
index 72dcbfde0..19c8a4f33 100644
--- a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift
+++ b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift
@@ -7,5 +7,5 @@ import Foundation
enum SystemEnvironment {
/// A Stream Chat version.
- public static let version: String = "4.78.0"
+ public static let version: String = "4.79.0"
}
diff --git a/Sources/StreamChatSwiftUI/Info.plist b/Sources/StreamChatSwiftUI/Info.plist
index 1bd3d64a3..01fdd70ce 100644
--- a/Sources/StreamChatSwiftUI/Info.plist
+++ b/Sources/StreamChatSwiftUI/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 4.78.0
+ 4.79.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSPhotoLibraryUsageDescription
diff --git a/Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.strings b/Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.strings
index 0eab6da4d..3d57da52a 100644
--- a/Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.strings
+++ b/Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.strings
@@ -90,6 +90,8 @@
"message.polls.votes" = "%d votes";
"message.translatedTo" = "Translated to %@";
+"message.showOriginal" = "Show Original";
+"message.showTranslation" = "Show Translation";
"alert.actions.cancel" = "Cancel";
"alert.actions.delete" = "Delete";
diff --git a/Sources/StreamChatSwiftUI/Utils.swift b/Sources/StreamChatSwiftUI/Utils.swift
index c36970e32..f39985f10 100644
--- a/Sources/StreamChatSwiftUI/Utils.swift
+++ b/Sources/StreamChatSwiftUI/Utils.swift
@@ -8,11 +8,14 @@ import StreamChat
/// Class providing implementations of several utilities used in the SDK.
/// The default implementations can be replaced in the init method, or directly via the variables.
public class Utils {
- // TODO: Make it public in future versions.
- internal var messagePreviewFormatter = MessagePreviewFormatter()
var markdownFormatter = MarkdownFormatter()
public var dateFormatter: DateFormatter
+
+ /// Date formatter where the format depends on the time passed.
+ ///
+ /// - SeeAlso: ``ChannelListConfig/messageRelativeDateFormatEnabled``.
+ public var messageRelativeDateFormatter: DateFormatter
public var videoPreviewLoader: VideoPreviewLoader
public var imageLoader: ImageLoading
public var imageCDN: ImageCDN
@@ -24,7 +27,9 @@ public class Utils {
public var channelAvatarsMerger: ChannelAvatarsMerging
public var messageTypeResolver: MessageTypeResolving
public var messageActionsResolver: MessageActionsResolving
+ public var messagePreviewFormatter: MessagePreviewFormatter
public var commandsConfig: CommandsConfig
+ public var channelListConfig: ChannelListConfig
public var messageListConfig: MessageListConfig
public var composerConfig: ComposerConfig
public var pollsConfig: PollsConfig
@@ -59,6 +64,8 @@ public class Utils {
public lazy var audioSessionFeedbackGenerator: AudioSessionFeedbackGenerator = StreamAudioSessionFeedbackGenerator()
+ public var originalTranslationsStore = MessageOriginalTranslationsStore()
+
var messageCachingUtils = MessageCachingUtils()
var messageListDateUtils: MessageListDateUtils
var channelControllerFactory = ChannelControllerFactory()
@@ -69,6 +76,7 @@ public class Utils {
public init(
dateFormatter: DateFormatter = .makeDefault(),
+ messageRelativeDateFormatter: DateFormatter = MessageRelativeDateFormatter(),
videoPreviewLoader: VideoPreviewLoader = DefaultVideoPreviewLoader(),
imageLoader: ImageLoading = NukeImageLoader(),
imageCDN: ImageCDN = StreamImageCDN(),
@@ -78,7 +86,9 @@ public class Utils {
channelAvatarsMerger: ChannelAvatarsMerging = ChannelAvatarsMerger(),
messageTypeResolver: MessageTypeResolving = MessageTypeResolver(),
messageActionResolver: MessageActionsResolving = MessageActionsResolver(),
+ messagePreviewFormatter: MessagePreviewFormatter = MessagePreviewFormatter(),
commandsConfig: CommandsConfig = DefaultCommandsConfig(),
+ channelListConfig: ChannelListConfig = ChannelListConfig(),
messageListConfig: MessageListConfig = MessageListConfig(),
composerConfig: ComposerConfig = ComposerConfig(),
pollsConfig: PollsConfig = PollsConfig(),
@@ -93,6 +103,7 @@ public class Utils {
shouldSyncChannelControllerOnAppear: @escaping (ChatChannelController) -> Bool = { _ in true }
) {
self.dateFormatter = dateFormatter
+ self.messageRelativeDateFormatter = messageRelativeDateFormatter
self.videoPreviewLoader = videoPreviewLoader
self.imageLoader = imageLoader
self.imageCDN = imageCDN
@@ -104,7 +115,9 @@ public class Utils {
self.channelAvatarsMerger = channelAvatarsMerger
self.messageTypeResolver = messageTypeResolver
messageActionsResolver = messageActionResolver
+ self.messagePreviewFormatter = messagePreviewFormatter
self.commandsConfig = commandsConfig
+ self.channelListConfig = channelListConfig
self.messageListConfig = messageListConfig
self.composerConfig = composerConfig
self.snapshotCreator = snapshotCreator
diff --git a/Sources/StreamChatSwiftUI/Utils/Common/ChatMessage+Extensions.swift b/Sources/StreamChatSwiftUI/Utils/Common/ChatMessage+Extensions.swift
index a44103587..41362caed 100644
--- a/Sources/StreamChatSwiftUI/Utils/Common/ChatMessage+Extensions.swift
+++ b/Sources/StreamChatSwiftUI/Utils/Common/ChatMessage+Extensions.swift
@@ -18,7 +18,13 @@ public extension ChatMessage {
/// A boolean value that checks if the last action (`send`, `edit` or `delete`) on the message failed.
var isLastActionFailed: Bool {
- guard isDeleted == false else { return false }
+ guard isDeleted == false else {
+ return false
+ }
+
+ if isBounced {
+ return true
+ }
switch localState {
case .sendingFailed, .syncingFailed, .deletingFailed:
diff --git a/Sources/StreamChatSwiftUI/Utils/Common/MessageRelativeDateFormatter.swift b/Sources/StreamChatSwiftUI/Utils/Common/MessageRelativeDateFormatter.swift
new file mode 100644
index 000000000..8c081dbf0
--- /dev/null
+++ b/Sources/StreamChatSwiftUI/Utils/Common/MessageRelativeDateFormatter.swift
@@ -0,0 +1,63 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+/// A formatter that converts message timestamps to a format which depends on the time passed.
+public final class MessageRelativeDateFormatter: DateFormatter, @unchecked Sendable {
+ override public init() {
+ super.init()
+ locale = .autoupdatingCurrent
+ dateStyle = .short
+ timeStyle = .none
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override public func string(from date: Date) -> String {
+ if calendar.isDateInToday(date) {
+ return todayFormatter.string(from: date)
+ }
+ if calendar.isDateInYesterday(date) {
+ return yesterdayFormatter.string(from: date)
+ }
+ if calendar.isDateInLastWeek(date) {
+ return weekdayFormatter.string(from: date)
+ }
+
+ return super.string(from: date)
+ }
+
+ var todayFormatter: DateFormatter {
+ InjectedValues[\.utils].dateFormatter
+ }
+
+ let yesterdayFormatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.locale = .autoupdatingCurrent
+ formatter.dateStyle = .short
+ formatter.timeStyle = .none
+ formatter.doesRelativeDateFormatting = true
+ return formatter
+ }()
+
+ let weekdayFormatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.locale = .autoupdatingCurrent
+ formatter.setLocalizedDateFormatFromTemplate("EEEE")
+ return formatter
+ }()
+}
+
+extension Calendar {
+ func isDateInLastWeek(_ date: Date) -> Bool {
+ guard let dateBefore7days = self.date(byAdding: .day, value: -7, to: Date()) else {
+ return false
+ }
+ return date > dateBefore7days
+ }
+}
diff --git a/Sources/StreamChatSwiftUI/Utils/MessageCachingUtils.swift b/Sources/StreamChatSwiftUI/Utils/MessageCachingUtils.swift
index f26aeb6bb..40320b29a 100644
--- a/Sources/StreamChatSwiftUI/Utils/MessageCachingUtils.swift
+++ b/Sources/StreamChatSwiftUI/Utils/MessageCachingUtils.swift
@@ -34,19 +34,22 @@ public struct UserDisplayInfo {
public let imageURL: URL?
public let role: UserRole?
public let size: CGSize?
+ public let extraData: [String: RawJSON]
public init(
id: String,
name: String,
imageURL: URL?,
role: UserRole? = nil,
- size: CGSize? = nil
+ size: CGSize? = nil,
+ extraData: [String: RawJSON] = [:]
) {
self.id = id
self.name = name
self.imageURL = imageURL
self.role = role
self.size = size
+ self.extraData = extraData
}
}
@@ -57,7 +60,8 @@ extension ChatMessage {
id: author.id,
name: author.name ?? author.id,
imageURL: author.imageURL,
- role: author.userRole
+ role: author.userRole,
+ extraData: author.extraData
)
}
diff --git a/Sources/StreamChatSwiftUI/Utils/MessagePreviewFormatter.swift b/Sources/StreamChatSwiftUI/Utils/MessagePreviewFormatter.swift
index 2a8d5a6bd..b4dadbdab 100644
--- a/Sources/StreamChatSwiftUI/Utils/MessagePreviewFormatter.swift
+++ b/Sources/StreamChatSwiftUI/Utils/MessagePreviewFormatter.swift
@@ -7,13 +7,13 @@ import SwiftUI
/// A formatter that converts a message to a text preview representation.
/// By default it is used to show message previews in the Channel List and Thread List.
-struct MessagePreviewFormatter {
+open class MessagePreviewFormatter {
@Injected(\.chatClient) var chatClient
- init() {}
+ public init() {}
/// Formats the message including the author's name.
- func format(_ previewMessage: ChatMessage, in channel: ChatChannel) -> String {
+ open func format(_ previewMessage: ChatMessage, in channel: ChatChannel) -> String {
if let poll = previewMessage.poll {
return formatPoll(poll)
}
@@ -21,7 +21,7 @@ struct MessagePreviewFormatter {
}
/// Formats only the content of the message without the author's name.
- func formatContent(for previewMessage: ChatMessage, in channel: ChatChannel) -> String {
+ open func formatContent(for previewMessage: ChatMessage, in channel: ChatChannel) -> String {
if let attachmentPreviewText = formatAttachmentContent(for: previewMessage, in: channel) {
return attachmentPreviewText
}
@@ -32,7 +32,7 @@ struct MessagePreviewFormatter {
}
/// Formats only the attachment content of the message in case it contains attachments.
- func formatAttachmentContent(for previewMessage: ChatMessage, in channel: ChatChannel) -> String? {
+ open func formatAttachmentContent(for previewMessage: ChatMessage, in channel: ChatChannel) -> String? {
if let poll = previewMessage.poll {
return "📊 \(poll.name)"
}
diff --git a/Sources/StreamChatSwiftUI/ViewFactory.swift b/Sources/StreamChatSwiftUI/ViewFactory.swift
index 762eaadc8..0aa9fbebd 100644
--- a/Sources/StreamChatSwiftUI/ViewFactory.swift
+++ b/Sources/StreamChatSwiftUI/ViewFactory.swift
@@ -359,6 +359,15 @@ public protocol ViewFactory: AnyObject {
/// - Returns: view shown in the date and author indicator slot.
func makeMessageAuthorAndDateView(for message: ChatMessage) -> MessageAuthorAndDateViewType
+ associatedtype MessageTranslationFooterViewType: View
+ /// Creates a view to display translation information below a message if it has been translated.
+ /// - Parameters:
+ /// - messageViewModel: The message view model used to display information about the message.
+ /// - Returns: A view to display translation information of the message.
+ func makeMessageTranslationFooterView(
+ messageViewModel: MessageViewModel
+ ) -> MessageTranslationFooterViewType
+
associatedtype LastInGroupHeaderView: View
/// Creates a view shown as a header of the last message in a group.
/// - Parameter message: the chat message for which the header will be displayed.
@@ -455,6 +464,19 @@ public protocol ViewFactory: AnyObject {
options: MediaViewsOptions
) -> GalleryViewType
+ associatedtype GalleryHeaderViewType: View
+ /// Creates the gallery header view presented with a sheet.
+ /// - Parameters:
+ /// - title: The title displayed in the header.
+ /// - subtitle: The subtitle displayed in the header.
+ /// - shown: Binding controlling whether the gallery is shown.
+ /// - Returns: View displayed in the gallery header slot.
+ func makeGalleryHeaderView(
+ title: String,
+ subtitle: String,
+ shown: Binding
+ ) -> GalleryHeaderViewType
+
associatedtype VideoPlayerViewType: View
/// Creates the video player view.
/// - Parameters:
@@ -469,6 +491,19 @@ public protocol ViewFactory: AnyObject {
isShown: Binding,
options: MediaViewsOptions
) -> VideoPlayerViewType
+
+ associatedtype VideoPlayerHeaderViewType: View
+ /// Creates the video player header view presented with a sheet.
+ /// - Parameters:
+ /// - title: The title displayed in the header.
+ /// - subtitle: The subtitle displayed in the header.
+ /// - shown: Binding controlling whether the video player is shown.
+ /// - Returns: View displayed in the video player header slot.
+ func makeVideoPlayerHeaderView(
+ title: String,
+ subtitle: String,
+ shown: Binding
+ ) -> VideoPlayerHeaderViewType
associatedtype DeletedMessageViewType: View
/// Creates the deleted message view.
diff --git a/StreamChatSwiftUI-XCFramework.podspec b/StreamChatSwiftUI-XCFramework.podspec
index 388996a85..80ce77419 100644
--- a/StreamChatSwiftUI-XCFramework.podspec
+++ b/StreamChatSwiftUI-XCFramework.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = 'StreamChatSwiftUI-XCFramework'
- spec.version = '4.78.0'
+ spec.version = '4.79.0'
spec.summary = 'StreamChat SwiftUI Chat Components'
spec.description = 'StreamChatSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamChat SDK.'
@@ -19,7 +19,7 @@ Pod::Spec.new do |spec|
spec.framework = 'Foundation', 'UIKit', 'SwiftUI'
- spec.dependency 'StreamChat-XCFramework', '~> 4.78.0'
+ spec.dependency 'StreamChat-XCFramework', '~> 4.79.0'
spec.cocoapods_version = '>= 1.11.0'
end
diff --git a/StreamChatSwiftUI.podspec b/StreamChatSwiftUI.podspec
index 4de9e323b..272dac1b7 100644
--- a/StreamChatSwiftUI.podspec
+++ b/StreamChatSwiftUI.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = 'StreamChatSwiftUI'
- spec.version = '4.78.0'
+ spec.version = '4.79.0'
spec.summary = 'StreamChat SwiftUI Chat Components'
spec.description = 'StreamChatSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamChat SDK.'
@@ -19,5 +19,5 @@ Pod::Spec.new do |spec|
spec.framework = 'Foundation', 'UIKit', 'SwiftUI'
- spec.dependency 'StreamChat', '~> 4.78.0'
+ spec.dependency 'StreamChat', '~> 4.79.0'
end
diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj
index 582fad6af..8df42398f 100644
--- a/StreamChatSwiftUI.xcodeproj/project.pbxproj
+++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj
@@ -16,10 +16,13 @@
4F6D83352C0F05040098C298 /* PollCommentsViewModel_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6D83342C0F05040098C298 /* PollCommentsViewModel_Tests.swift */; };
4F6D83512C1079A00098C298 /* AlertBannerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6D83502C1079A00098C298 /* AlertBannerViewModifier.swift */; };
4F6D83542C1094220098C298 /* AlertBannerViewModifier_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6D83532C1094220098C298 /* AlertBannerViewModifier_Tests.swift */; };
+ 4F7613792DDCB2C900F996E3 /* MessageRelativeDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7613782DDCB2AD00F996E3 /* MessageRelativeDateFormatter.swift */; };
4F7720AE2C58C45200BAEC02 /* OnLoadViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7720AD2C58C45000BAEC02 /* OnLoadViewModifier.swift */; };
4F7DD9A02BFC7C6100599AA6 /* ChatClient+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7DD99F2BFC7C6100599AA6 /* ChatClient+Extensions.swift */; };
4F7DD9A22BFCB2EF00599AA6 /* ChatClientExtensions_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7DD9A12BFCB2EF00599AA6 /* ChatClientExtensions_Tests.swift */; };
4F889C562D7F000700A7BDAF /* ChatMessageExtensions_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F889C552D7F000700A7BDAF /* ChatMessageExtensions_Tests.swift */; };
+ 4F8D64402DDDCF9300026C09 /* MessageRelativeDateFormatter_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8D643F2DDDCF9300026C09 /* MessageRelativeDateFormatter_Tests.swift */; };
+ 4F9173FD2DDDFFE8003C30B5 /* ChannelListConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F9173FC2DDDFFE3003C30B5 /* ChannelListConfig.swift */; };
4FA3741A2D799CA400294721 /* AppConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA374192D799CA400294721 /* AppConfigurationView.swift */; };
4FA3741D2D799FC300294721 /* AppConfigurationTranslationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA3741C2D799FC300294721 /* AppConfigurationTranslationView.swift */; };
4FA3741F2D79A64F00294721 /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA3741E2D79A64900294721 /* AppConfiguration.swift */; };
@@ -522,6 +525,7 @@
AD3AB65C2CB730090014D4D7 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3AB65B2CB730090014D4D7 /* Shimmer.swift */; };
AD3AB65E2CB731360014D4D7 /* ChatThreadListLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3AB65D2CB731360014D4D7 /* ChatThreadListLoadingView.swift */; };
AD3AB6602CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3AB65F2CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift */; };
+ AD51D9182DB9543A0068D0B0 /* MessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD51D9172DB9543A0068D0B0 /* MessageViewModel.swift */; };
AD5C0A5F2D6FDD9700E1E500 /* BouncedMessageActionsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD5C0A5E2D6FDD8600E1E500 /* BouncedMessageActionsModifier.swift */; };
AD6B7E052D356E8800ADEF39 /* ReactionsUsersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6B7E042D356E8800ADEF39 /* ReactionsUsersViewModel.swift */; };
ADE0F55E2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */; };
@@ -532,6 +536,7 @@
ADE442EE2CBDAAAA0066CDF7 /* ChatThreadListViewModel_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE442ED2CBDAAAA0066CDF7 /* ChatThreadListViewModel_Tests.swift */; };
ADE442F02CBDAAB70066CDF7 /* ChatThreadListView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE442EF2CBDAAB70066CDF7 /* ChatThreadListView_Tests.swift */; };
ADE442F22CBDAAC40066CDF7 /* ChatThreadListItemView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE442F12CBDAAC40066CDF7 /* ChatThreadListItemView_Tests.swift */; };
+ ADF544382DC93C2A0024A0B3 /* MessageTranslationFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF544372DC93C0D0024A0B3 /* MessageTranslationFooterView.swift */; };
C14A465B284665B100EF498E /* SDKIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14A465A284665B100EF498E /* SDKIdentifier.swift */; };
E3A1C01C282BAC66002D1E26 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = E3A1C01B282BAC66002D1E26 /* Sentry */; };
/* End PBXBuildFile section */
@@ -612,10 +617,13 @@
4F6D83342C0F05040098C298 /* PollCommentsViewModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollCommentsViewModel_Tests.swift; sourceTree = ""; };
4F6D83502C1079A00098C298 /* AlertBannerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertBannerViewModifier.swift; sourceTree = ""; };
4F6D83532C1094220098C298 /* AlertBannerViewModifier_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertBannerViewModifier_Tests.swift; sourceTree = ""; };
+ 4F7613782DDCB2AD00F996E3 /* MessageRelativeDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRelativeDateFormatter.swift; sourceTree = ""; };
4F7720AD2C58C45000BAEC02 /* OnLoadViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnLoadViewModifier.swift; sourceTree = ""; };
4F7DD99F2BFC7C6100599AA6 /* ChatClient+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatClient+Extensions.swift"; sourceTree = ""; };
4F7DD9A12BFCB2EF00599AA6 /* ChatClientExtensions_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatClientExtensions_Tests.swift; sourceTree = ""; };
4F889C552D7F000700A7BDAF /* ChatMessageExtensions_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageExtensions_Tests.swift; sourceTree = ""; };
+ 4F8D643F2DDDCF9300026C09 /* MessageRelativeDateFormatter_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRelativeDateFormatter_Tests.swift; sourceTree = ""; };
+ 4F9173FC2DDDFFE3003C30B5 /* ChannelListConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelListConfig.swift; sourceTree = ""; };
4FA374192D799CA400294721 /* AppConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationView.swift; sourceTree = ""; };
4FA3741C2D799FC300294721 /* AppConfigurationTranslationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationTranslationView.swift; sourceTree = ""; };
4FA3741E2D79A64900294721 /* AppConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfiguration.swift; sourceTree = ""; };
@@ -1123,6 +1131,7 @@
AD3AB65B2CB730090014D4D7 /* Shimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shimmer.swift; sourceTree = ""; };
AD3AB65D2CB731360014D4D7 /* ChatThreadListLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListLoadingView.swift; sourceTree = ""; };
AD3AB65F2CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListHeaderViewModifier.swift; sourceTree = ""; };
+ AD51D9172DB9543A0068D0B0 /* MessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = ""; };
AD5C0A5E2D6FDD8600E1E500 /* BouncedMessageActionsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BouncedMessageActionsModifier.swift; sourceTree = ""; };
AD6B7E042D356E8800ADEF39 /* ReactionsUsersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsUsersViewModel.swift; sourceTree = ""; };
ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListErrorBannerView.swift; sourceTree = ""; };
@@ -1133,6 +1142,7 @@
ADE442ED2CBDAAAA0066CDF7 /* ChatThreadListViewModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListViewModel_Tests.swift; sourceTree = ""; };
ADE442EF2CBDAAB70066CDF7 /* ChatThreadListView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListView_Tests.swift; sourceTree = ""; };
ADE442F12CBDAAC40066CDF7 /* ChatThreadListItemView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListItemView_Tests.swift; sourceTree = ""; };
+ ADF544372DC93C0D0024A0B3 /* MessageTranslationFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTranslationFooterView.swift; sourceTree = ""; };
C14A465A284665B100EF498E /* SDKIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKIdentifier.swift; sourceTree = ""; };
/* End PBXFileReference section */
@@ -1786,8 +1796,10 @@
8465FCFF2746A95600AF091E /* MessageListView.swift */,
8465FD052746A95600AF091E /* MessageContainerView.swift */,
8465FD0E2746A95600AF091E /* MessageView.swift */,
+ AD51D9172DB9543A0068D0B0 /* MessageViewModel.swift */,
84DEC8E02760D24100172876 /* MessageRepliesView.swift */,
8465FD0D2746A95600AF091E /* MessageAvatarView.swift */,
+ ADF544372DC93C0D0024A0B3 /* MessageTranslationFooterView.swift */,
8465FCFE2746A95600AF091E /* ImageAttachmentView.swift */,
84A75FBA274EA29B00225CE8 /* GiphyAttachmentView.swift */,
8465FD002746A95600AF091E /* FileAttachmentPreview.swift */,
@@ -1935,6 +1947,7 @@
8465FD3D2746A95600AF091E /* ImageCDN.swift */,
8465FD482746A95600AF091E /* ImageMerger.swift */,
8465FD422746A95600AF091E /* InputTextView.swift */,
+ 4F7613782DDCB2AD00F996E3 /* MessageRelativeDateFormatter.swift */,
8465FD432746A95600AF091E /* NSLayoutConstraint+Extensions.swift */,
8465FD492746A95600AF091E /* NukeImageProcessor.swift */,
4F7720AD2C58C45000BAEC02 /* OnLoadViewModifier.swift */,
@@ -1951,22 +1964,23 @@
8465FD4C2746A95600AF091E /* ChatChannelList */ = {
isa = PBXGroup;
children = (
+ 8465FD4D2746A95600AF091E /* ChannelAvatarsMerger.swift */,
+ 8465FD572746A95700AF091E /* ChannelHeaderLoader.swift */,
+ 4F9173FC2DDDFFE3003C30B5 /* ChannelListConfig.swift */,
+ 8465FD4E2746A95600AF091E /* ChatChannelHelperViews.swift */,
+ 8465FD512746A95600AF091E /* ChatChannelList.swift */,
+ 8465FD542746A95700AF091E /* ChatChannelListHeader.swift */,
+ 8465FD592746A95700AF091E /* ChatChannelListItem.swift */,
8465FD552746A95700AF091E /* ChatChannelListScreen.swift */,
8465FD5C2746A95700AF091E /* ChatChannelListView.swift */,
- 8465FD512746A95600AF091E /* ChatChannelList.swift */,
8465FD582746A95700AF091E /* ChatChannelListViewModel.swift */,
- 8465FD542746A95700AF091E /* ChatChannelListHeader.swift */,
- 8465FD5A2746A95700AF091E /* ChatChannelSwipeableListItem.swift */,
8465FD532746A95600AF091E /* ChatChannelNavigatableListItem.swift */,
- 8465FD592746A95700AF091E /* ChatChannelListItem.swift */,
- 8465FD4D2746A95600AF091E /* ChannelAvatarsMerger.swift */,
- 8465FD4E2746A95600AF091E /* ChatChannelHelperViews.swift */,
+ 8465FD5A2746A95700AF091E /* ChatChannelSwipeableListItem.swift */,
8465FD502746A95600AF091E /* DefaultChannelActions.swift */,
- 8465FD522746A95600AF091E /* NoChannelsView.swift */,
- 8465FD572746A95700AF091E /* ChannelHeaderLoader.swift */,
+ 91B763A3283EB19800B458A9 /* MoreChannelActionsFullScreenWrappingView.swift */,
8465FD4F2746A95600AF091E /* MoreChannelActionsView.swift */,
8465FD5B2746A95700AF091E /* MoreChannelActionsViewModel.swift */,
- 91B763A3283EB19800B458A9 /* MoreChannelActionsFullScreenWrappingView.swift */,
+ 8465FD522746A95600AF091E /* NoChannelsView.swift */,
8421BCEF27A44EAE000F977D /* SearchResultsView.swift */,
);
path = ChatChannelList;
@@ -2186,6 +2200,7 @@
91B79FD8284E7E9C005B6E4F /* ChatUserNamer_Tests.swift */,
84C94D53275A1380007FE2B9 /* DateUtils_Tests.swift */,
84C94D5D275A3AA9007FE2B9 /* ImageCDN_Tests.swift */,
+ 4F8D643F2DDDCF9300026C09 /* MessageRelativeDateFormatter_Tests.swift */,
849988AF2AE6BE4800CC95C9 /* PaddingsConfig_Tests.swift */,
84779C762AEBCA6E000A6A68 /* ReactionsIconProvider_Tests.swift */,
84E1D8272976CCAF00060491 /* SortReactions_Tests.swift */,
@@ -2675,6 +2690,7 @@
82D64BEA2AD7E5B700C5C79E /* TaskFetchOriginalImageData.swift in Sources */,
82D64BDB2AD7E5B700C5C79E /* UIImageView.swift in Sources */,
82D64BD02AD7E5B700C5C79E /* NukeVideoPlayerView.swift in Sources */,
+ ADF544382DC93C2A0024A0B3 /* MessageTranslationFooterView.swift in Sources */,
84EADEB52B28935B0046B50C /* RecordingView.swift in Sources */,
84AB7B262773619F00631A10 /* MentionUsersView.swift in Sources */,
8465FDA82746A95700AF091E /* ImageLoading.swift in Sources */,
@@ -2790,9 +2806,11 @@
82D64C0C2AD7E5B700C5C79E /* ImageEncoders.swift in Sources */,
8465FD832746A95700AF091E /* LinkAttachmentView.swift in Sources */,
4FCD7DA72D632121000EEB0F /* MarkdownFormatter.swift in Sources */,
+ AD51D9182DB9543A0068D0B0 /* MessageViewModel.swift in Sources */,
82D64C082AD7E5B700C5C79E /* Operation.swift in Sources */,
82D64BEB2AD7E5B700C5C79E /* ImagePipelineTask.swift in Sources */,
AD2DDA612CB040EA0040B8D4 /* NoThreadsView.swift in Sources */,
+ 4F9173FD2DDDFFE8003C30B5 /* ChannelListConfig.swift in Sources */,
82D64BF92AD7E5B700C5C79E /* ImageProcessors+Resize.swift in Sources */,
8465FDC22746A95700AF091E /* ChatChannelNavigatableListItem.swift in Sources */,
8465FDAD2746A95700AF091E /* ImageCDN.swift in Sources */,
@@ -2806,6 +2824,7 @@
8465FDA52746A95700AF091E /* Modifiers.swift in Sources */,
8465FDBB2746A95700AF091E /* LoadingView.swift in Sources */,
84D6E4F62B2CA4E300D0056C /* RecordingTipView.swift in Sources */,
+ 4F7613792DDCB2C900F996E3 /* MessageRelativeDateFormatter.swift in Sources */,
846608E3278C303800D3D7B3 /* TypingIndicatorView.swift in Sources */,
84A1CACF2816BCF00046595A /* AddUsersView.swift in Sources */,
82D64BF02AD7E5B700C5C79E /* DataLoading.swift in Sources */,
@@ -3065,6 +3084,7 @@
84B2B5CA281947E100479CEE /* ViewFrameUtils.swift in Sources */,
8423C342277CBA280092DCF1 /* TypingSuggester_Tests.swift in Sources */,
84507C9A281ACCD70081DDC2 /* AddUsersView_Tests.swift in Sources */,
+ 4F8D64402DDDCF9300026C09 /* MessageRelativeDateFormatter_Tests.swift in Sources */,
84C94D0627578BF2007FE2B9 /* UnwrapAsync.swift in Sources */,
84D6B55A27DF6EC7009C6D07 /* LoadingView_Tests.swift in Sources */,
84C94D0427578BF2007FE2B9 /* TestError.swift in Sources */,
@@ -3880,7 +3900,7 @@
repositoryURL = "https://github.com/GetStream/stream-chat-swift.git";
requirement = {
kind = upToNextMajorVersion;
- minimumVersion = 4.78.0;
+ minimumVersion = 4.79.0;
};
};
E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
diff --git a/StreamChatSwiftUIArtifacts.json b/StreamChatSwiftUIArtifacts.json
index 74ca01551..9c9ffb5b5 100644
--- a/StreamChatSwiftUIArtifacts.json
+++ b/StreamChatSwiftUIArtifacts.json
@@ -1 +1 @@
-{"4.40.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.40.0/StreamChatSwiftUI.zip","4.41.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.41.0/StreamChatSwiftUI.zip","4.42.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.42.0/StreamChatSwiftUI.zip","4.43.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.43.0/StreamChatSwiftUI.zip","4.44.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.44.0/StreamChatSwiftUI.zip","4.45.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.45.0/StreamChatSwiftUI.zip","4.46.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.46.0/StreamChatSwiftUI.zip","4.47.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.0/StreamChatSwiftUI.zip","4.47.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.1/StreamChatSwiftUI.zip","4.48.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.48.0/StreamChatSwiftUI.zip","4.49.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.49.0/StreamChatSwiftUI.zip","4.50.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.0/StreamChatSwiftUI.zip","4.50.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.1/StreamChatSwiftUI.zip","4.51.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.51.0/StreamChatSwiftUI.zip","4.52.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.52.0/StreamChatSwiftUI.zip","4.53.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.53.0/StreamChatSwiftUI.zip","4.54.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.54.0/StreamChatSwiftUI.zip","4.55.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.55.0/StreamChatSwiftUI.zip","4.56.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.56.0/StreamChatSwiftUI.zip","4.57.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.57.0/StreamChatSwiftUI.zip","4.58.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.58.0/StreamChatSwiftUI.zip","4.59.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.59.0/StreamChatSwiftUI.zip","4.60.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.60.0/StreamChatSwiftUI.zip","4.61.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.61.0/StreamChatSwiftUI.zip","4.62.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.62.0/StreamChatSwiftUI.zip","4.63.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.63.0/StreamChatSwiftUI.zip","4.64.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.64.0/StreamChatSwiftUI.zip","4.65.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.65.0/StreamChatSwiftUI.zip","4.66.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.66.0/StreamChatSwiftUI.zip","4.67.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.67.0/StreamChatSwiftUI.zip","4.68.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.68.0/StreamChatSwiftUI.zip","4.69.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.69.0/StreamChatSwiftUI.zip","4.70.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.70.0/StreamChatSwiftUI.zip","4.71.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.71.0/StreamChatSwiftUI.zip","4.72.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.72.0/StreamChatSwiftUI.zip","4.73.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.73.0/StreamChatSwiftUI.zip","4.74.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.74.0/StreamChatSwiftUI.zip","4.75.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.75.0/StreamChatSwiftUI.zip","4.76.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.76.0/StreamChatSwiftUI.zip","4.77.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.77.0/StreamChatSwiftUI.zip","4.78.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.78.0/StreamChatSwiftUI.zip"}
\ No newline at end of file
+{"4.40.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.40.0/StreamChatSwiftUI.zip","4.41.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.41.0/StreamChatSwiftUI.zip","4.42.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.42.0/StreamChatSwiftUI.zip","4.43.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.43.0/StreamChatSwiftUI.zip","4.44.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.44.0/StreamChatSwiftUI.zip","4.45.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.45.0/StreamChatSwiftUI.zip","4.46.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.46.0/StreamChatSwiftUI.zip","4.47.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.0/StreamChatSwiftUI.zip","4.47.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.1/StreamChatSwiftUI.zip","4.48.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.48.0/StreamChatSwiftUI.zip","4.49.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.49.0/StreamChatSwiftUI.zip","4.50.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.0/StreamChatSwiftUI.zip","4.50.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.1/StreamChatSwiftUI.zip","4.51.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.51.0/StreamChatSwiftUI.zip","4.52.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.52.0/StreamChatSwiftUI.zip","4.53.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.53.0/StreamChatSwiftUI.zip","4.54.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.54.0/StreamChatSwiftUI.zip","4.55.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.55.0/StreamChatSwiftUI.zip","4.56.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.56.0/StreamChatSwiftUI.zip","4.57.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.57.0/StreamChatSwiftUI.zip","4.58.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.58.0/StreamChatSwiftUI.zip","4.59.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.59.0/StreamChatSwiftUI.zip","4.60.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.60.0/StreamChatSwiftUI.zip","4.61.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.61.0/StreamChatSwiftUI.zip","4.62.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.62.0/StreamChatSwiftUI.zip","4.63.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.63.0/StreamChatSwiftUI.zip","4.64.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.64.0/StreamChatSwiftUI.zip","4.65.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.65.0/StreamChatSwiftUI.zip","4.66.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.66.0/StreamChatSwiftUI.zip","4.67.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.67.0/StreamChatSwiftUI.zip","4.68.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.68.0/StreamChatSwiftUI.zip","4.69.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.69.0/StreamChatSwiftUI.zip","4.70.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.70.0/StreamChatSwiftUI.zip","4.71.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.71.0/StreamChatSwiftUI.zip","4.72.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.72.0/StreamChatSwiftUI.zip","4.73.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.73.0/StreamChatSwiftUI.zip","4.74.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.74.0/StreamChatSwiftUI.zip","4.75.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.75.0/StreamChatSwiftUI.zip","4.76.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.76.0/StreamChatSwiftUI.zip","4.77.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.77.0/StreamChatSwiftUI.zip","4.78.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.78.0/StreamChatSwiftUI.zip","4.79.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.79.0/StreamChatSwiftUI.zip"}
\ No newline at end of file
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActionsViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActionsViewModel_Tests.swift
index 20e07d4d6..3daff5bf7 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActionsViewModel_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActionsViewModel_Tests.swift
@@ -20,7 +20,7 @@ class MessageActionsViewModel_Tests: StreamChatTestCase {
text: "test",
author: .mock(id: .unique)
),
- channel: .mockDMChannel(ownCapabilities: [.sendMessage, .uploadFile, .pinMessage]),
+ channel: .mockDMChannel(ownCapabilities: [.sendMessage, .uploadFile, .pinMessage, .readEvents]),
chatClient: chatClient,
onFinish: { _ in },
onError: { _ in }
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift
index 83e10a087..29e5475a0 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageActions_Tests.swift
@@ -72,7 +72,38 @@ class MessageActions_Tests: StreamChatTestCase {
XCTAssert(messageActions[4].title == "Mark Unread")
XCTAssert(messageActions[5].title == "Mute User")
}
-
+
+ func test_messageActions_otherUserDefaultReadEventsDisabled() {
+ // Given
+ let channel = ChatChannel.mockDMChannel(ownCapabilities: [.sendMessage, .uploadFile, .pinMessage])
+ let message = ChatMessage.mock(
+ id: .unique,
+ cid: channel.cid,
+ text: "Test",
+ author: .mock(id: .unique),
+ isSentByCurrentUser: false
+ )
+ let factory = DefaultViewFactory.shared
+
+ // When
+ let messageActions = MessageAction.defaultActions(
+ factory: factory,
+ for: message,
+ channel: channel,
+ chatClient: chatClient,
+ onFinish: { _ in },
+ onError: { _ in }
+ )
+
+ // Then
+ XCTAssert(messageActions.count == 5)
+ XCTAssert(messageActions[0].title == "Reply")
+ XCTAssert(messageActions[1].title == "Thread Reply")
+ XCTAssert(messageActions[2].title == "Pin to conversation")
+ XCTAssert(messageActions[3].title == "Copy Message")
+ XCTAssert(messageActions[4].title == "Mute User")
+ }
+
func test_messageActions_otherUserDefaultBlockingEnabled() {
// Given
streamChat = StreamChat(
@@ -292,12 +323,64 @@ class MessageActions_Tests: StreamChatTestCase {
XCTAssertEqual(messageActions[3].title, "Copy Message")
XCTAssertEqual(messageActions[4].title, "Delete Message")
}
+
+ func test_messageActions_currentUser_editingDisabledWhenNoUpdateCapabilities() {
+ // Given
+ let channel = ChatChannel.mockDMChannel(ownCapabilities: [])
+ let message = ChatMessage.mock(
+ id: .unique,
+ cid: channel.cid,
+ text: "Test",
+ author: .mock(id: chatClient.currentUserId!),
+ isSentByCurrentUser: true
+ )
+ let factory = DefaultViewFactory.shared
+
+ // When
+ let messageActions = MessageAction.defaultActions(
+ factory: factory,
+ for: message,
+ channel: channel,
+ chatClient: chatClient,
+ onFinish: { _ in },
+ onError: { _ in }
+ )
+
+ // Then
+ XCTAssertFalse(messageActions.contains(where: { $0.title == "Edit Message" }))
+ }
+
+ func test_messageActions_otherUser_editingEnabledWhenUpdateAnyMessageCapability() {
+ // Given
+ let channel = ChatChannel.mockDMChannel(ownCapabilities: [.updateAnyMessage])
+ let message = ChatMessage.mock(
+ id: .unique,
+ cid: channel.cid,
+ text: "Test",
+ author: .mock(id: .unique),
+ isSentByCurrentUser: false
+ )
+ let factory = DefaultViewFactory.shared
+
+ // When
+ let messageActions = MessageAction.defaultActions(
+ factory: factory,
+ for: message,
+ channel: channel,
+ chatClient: chatClient,
+ onFinish: { _ in },
+ onError: { _ in }
+ )
+
+ // Then
+ XCTAssertTrue(messageActions.contains(where: { $0.title == "Edit Message" }))
+ }
// MARK: - Private
private var mockDMChannel: ChatChannel {
ChatChannel.mockDMChannel(
- ownCapabilities: [.sendMessage, .uploadFile, .pinMessage]
+ ownCapabilities: [.updateOwnMessage, .sendMessage, .uploadFile, .pinMessage, .readEvents]
)
}
}
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift
index fc8501058..eb43ea43e 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift
@@ -659,6 +659,106 @@ class MessageComposerViewModel_Tests: StreamChatTestCase {
XCTAssertFalse(viewModel.canSendPoll)
}
+ func test_showCommandsOverlay() {
+ // Given
+ let channelController = makeChannelController()
+ let messageController = ChatMessageControllerSUI_Mock.mock(
+ chatClient: chatClient,
+ cid: .unique,
+ messageId: .unique
+ )
+ let viewModel = MessageComposerViewModel(
+ channelController: channelController,
+ messageController: messageController
+ )
+
+ // When
+ let channelConfig = ChannelConfig(commands: [.init()])
+ channelController.channel_mock = .mock(
+ cid: .unique,
+ config: channelConfig
+ )
+ viewModel.composerCommand = .init(id: "test", typingSuggestion: .empty, displayInfo: nil)
+
+ // Then
+ XCTAssertTrue(viewModel.showCommandsOverlay)
+ }
+
+ func test_showCommandsOverlay_whenComposerCommandIsNil_returnsFalse() {
+ // Given
+ let channelController = makeChannelController()
+ let messageController = ChatMessageControllerSUI_Mock.mock(
+ chatClient: chatClient,
+ cid: .unique,
+ messageId: .unique
+ )
+ let viewModel = MessageComposerViewModel(
+ channelController: channelController,
+ messageController: messageController
+ )
+
+ // When
+ let channelConfig = ChannelConfig(commands: [.init()])
+ channelController.channel_mock = .mock(
+ cid: .unique,
+ config: channelConfig
+ )
+ viewModel.composerCommand = nil
+
+ // Then
+ XCTAssertFalse(viewModel.showCommandsOverlay)
+ }
+
+ func test_showCommandsOverlay_whenCommandsAreDisabled_returnsFalse() {
+ // Given
+ let channelController = makeChannelController()
+ let messageController = ChatMessageControllerSUI_Mock.mock(
+ chatClient: chatClient,
+ cid: .unique,
+ messageId: .unique
+ )
+ let viewModel = MessageComposerViewModel(
+ channelController: channelController,
+ messageController: messageController
+ )
+
+ // When
+ let channelConfig = ChannelConfig(commands: [])
+ channelController.channel_mock = .mock(
+ cid: .unique,
+ config: channelConfig
+ )
+ viewModel.composerCommand = .init(id: "test", typingSuggestion: .empty, displayInfo: nil)
+
+ // Then
+ XCTAssertFalse(viewModel.showCommandsOverlay)
+ }
+
+ func test_showCommandsOverlay_whenCommandsAreDisabledButIsMentions_returnsTrue() {
+ // Given
+ let channelController = makeChannelController()
+ let messageController = ChatMessageControllerSUI_Mock.mock(
+ chatClient: chatClient,
+ cid: .unique,
+ messageId: .unique
+ )
+ let viewModel = MessageComposerViewModel(
+ channelController: channelController,
+ messageController: messageController
+ )
+
+ // When
+ let channelConfig = ChannelConfig(commands: [])
+ channelController.channel_mock = .mock(
+ cid: .unique,
+ config: channelConfig
+ )
+ viewModel.composerCommand = .init(id: "mentions", typingSuggestion: .empty, displayInfo: nil)
+
+ // Then
+ XCTAssertTrue(viewModel.showCommandsOverlay)
+ }
+
func test_addedAsset_extraData() {
// Given
let image = UIImage(systemName: "person")!
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift
index bbd514b6f..2afea3e2a 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift
@@ -5,6 +5,7 @@
import SnapshotTesting
@testable import StreamChat
@testable import StreamChatSwiftUI
+@testable import StreamChatTestTools
import StreamSwiftTestHelpers
import SwiftUI
import XCTest
@@ -123,7 +124,7 @@ class MessageContainerView_Tests: StreamChatTestCase {
)
// When
- let view = testMessageViewContainer(message: message)
+ let view = testMessageViewContainer(message: message, channel: .mockNonDMChannel())
// Then
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
@@ -139,7 +140,7 @@ class MessageContainerView_Tests: StreamChatTestCase {
)
// When
- let view = testMessageViewContainer(message: message)
+ let view = testMessageViewContainer(message: message, channel: .mockNonDMChannel())
// Then
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
@@ -160,7 +161,43 @@ class MessageContainerView_Tests: StreamChatTestCase {
)
// When
- let view = testMessageViewContainer(message: message)
+ let view = testMessageViewContainer(message: message, channel: .mockNonDMChannel())
+
+ // Then
+ assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
+ }
+
+ func test_messageContainerView_sendingFailed_snapshot() {
+ // Given
+ let message = ChatMessage.mock(
+ id: .unique,
+ cid: .unique,
+ text: "Test message",
+ author: .mock(id: .unique),
+ attachments: [],
+ localState: .sendingFailed
+ )
+
+ // When
+ let view = testMessageViewContainer(message: message, channel: .mockNonDMChannel())
+
+ // Then
+ assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
+ }
+
+ func test_messageContainerView_editingFailed_snapshot() {
+ // Given
+ let message = ChatMessage.mock(
+ id: .unique,
+ cid: .unique,
+ text: "Test message",
+ author: .mock(id: .unique),
+ attachments: [],
+ localState: .syncingFailed
+ )
+
+ // When
+ let view = testMessageViewContainer(message: message, channel: .mockNonDMChannel())
// Then
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
@@ -274,7 +311,7 @@ class MessageContainerView_Tests: StreamChatTestCase {
)
// When
- let view = testMessageViewContainer(message: message)
+ let view = testMessageViewContainer(message: message, channel: .mockNonDMChannel())
// Then
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
@@ -292,7 +329,7 @@ class MessageContainerView_Tests: StreamChatTestCase {
)
// When
- let view = testMessageViewContainer(message: message)
+ let view = testMessageViewContainer(message: message, channel: .mockNonDMChannel())
// Then
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
@@ -315,7 +352,7 @@ class MessageContainerView_Tests: StreamChatTestCase {
.environment(\.channelTranslationLanguage, .spanish)
// Then
- AssertSnapshot(view, size: CGSize(width: 375, height: 200))
+ assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
}
func test_translatedText_myMessageIsNotTranslated_snapshot() {
@@ -412,20 +449,9 @@ class MessageContainerView_Tests: StreamChatTestCase {
isSentByCurrentUser: true
)
- let view = MessageContainerView(
- factory: DefaultViewFactory.shared,
- channel: .mockDMChannel(config: .mock(repliesEnabled: true)),
- message: message,
- width: defaultScreenSize.width,
- showsAllInfo: true,
- isInThread: false,
- isLast: false,
- scrolledId: .constant(nil),
- quotedMessage: .constant(nil),
- onLongPress: { _ in }
- )
+ let viewModel = MessageViewModel(message: message, channel: .mockDMChannel(config: .mock(repliesEnabled: true)))
- XCTAssertTrue(view.isSwipeToReplyPossible)
+ XCTAssertTrue(viewModel.isSwipeToQuoteReplyPossible)
}
func test_isSwipeToReplyPossible_whenRepliesDisabled_whenMessageInteractable_shouldBeFalse() {
@@ -437,20 +463,9 @@ class MessageContainerView_Tests: StreamChatTestCase {
isSentByCurrentUser: true
)
- let view = MessageContainerView(
- factory: DefaultViewFactory.shared,
- channel: .mockDMChannel(config: .mock(repliesEnabled: false)),
- message: message,
- width: defaultScreenSize.width,
- showsAllInfo: true,
- isInThread: false,
- isLast: false,
- scrolledId: .constant(nil),
- quotedMessage: .constant(nil),
- onLongPress: { _ in }
- )
+ let viewModel = MessageViewModel(message: message, channel: .mockDMChannel(config: .mock(repliesEnabled: false)))
- XCTAssertFalse(view.isSwipeToReplyPossible)
+ XCTAssertFalse(viewModel.isSwipeToQuoteReplyPossible)
}
func test_isSwipeToReplyPossible_whenRepliesEnabled_whenMessageNotInteractable_shouldBeFalse() {
@@ -463,28 +478,125 @@ class MessageContainerView_Tests: StreamChatTestCase {
isSentByCurrentUser: true
)
- let view = MessageContainerView(
- factory: DefaultViewFactory.shared,
- channel: .mockDMChannel(config: .mock(repliesEnabled: true)),
+ let viewModel = MessageViewModel(message: message, channel: .mockDMChannel(config: .mock(repliesEnabled: true)))
+
+ XCTAssertFalse(viewModel.isSwipeToQuoteReplyPossible)
+ }
+
+ func test_translatedText_showOriginalTranslatedButtonEnabled_originalTextShown_snapshot() {
+ // Given
+ let message = ChatMessage.mock(
+ id: .unique,
+ cid: .unique,
+ text: "Hello",
+ author: .mock(id: .unique),
+ translations: [
+ .spanish: "Hola"
+ ]
+ )
+ let utils = Utils(
+ dateFormatter: EmptyDateFormatter(),
+ messageListConfig: .init(
+ messageDisplayOptions: MessageDisplayOptions(showOriginalTranslatedButton: true)
+ )
+ )
+ streamChat = StreamChat(chatClient: chatClient, utils: utils)
+
+ // When
+ let messageViewModel = MessageViewModel_Mock(
message: message,
- width: defaultScreenSize.width,
- showsAllInfo: true,
- isInThread: false,
- isLast: false,
- scrolledId: .constant(nil),
- quotedMessage: .constant(nil),
- onLongPress: { _ in }
+ channel: .mock(
+ cid: .unique,
+ membership: .mock(id: .unique, language: .spanish)
+ )
+ )
+ messageViewModel.mockOriginalTextShown = true
+ let view = testMessageViewContainer(message: message, messageViewModel: messageViewModel)
+ .environment(\.channelTranslationLanguage, .spanish)
+
+ // Then
+ assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
+ }
+
+ func test_translatedText_showOriginalTranslatedButtonEnabled_translatedTextShown_snapshot() {
+ // Given
+ let message = ChatMessage.mock(
+ id: .unique,
+ cid: .unique,
+ text: "Hello",
+ author: .mock(id: .unique),
+ translations: [
+ .spanish: "Hola"
+ ]
+ )
+ let utils = Utils(
+ dateFormatter: EmptyDateFormatter(),
+ messageListConfig: .init(
+ messageDisplayOptions: MessageDisplayOptions(showOriginalTranslatedButton: true)
+ )
+ )
+ streamChat = StreamChat(chatClient: chatClient, utils: utils)
+
+ // When
+ let messageViewModel = MessageViewModel_Mock(
+ message: message,
+ channel: .mock(
+ cid: .unique,
+ membership: .mock(id: .unique, language: .spanish)
+ )
)
+ messageViewModel.mockOriginalTextShown = false
+ let view = testMessageViewContainer(message: message, messageViewModel: messageViewModel)
+ .environment(\.channelTranslationLanguage, .spanish)
- XCTAssertFalse(view.isSwipeToReplyPossible)
+ // Then
+ assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
+ }
+
+ func test_translatedText_showOriginalTranslatedButtonDisabled_translatedTextShown_snapshot() {
+ // Given
+ let message = ChatMessage.mock(
+ id: .unique,
+ cid: .unique,
+ text: "Hello",
+ author: .mock(id: .unique),
+ translations: [
+ .spanish: "Hola"
+ ]
+ )
+ let utils = Utils(
+ dateFormatter: EmptyDateFormatter(),
+ messageListConfig: .init(
+ messageDisplayOptions: MessageDisplayOptions(showOriginalTranslatedButton: false)
+ )
+ )
+ streamChat = StreamChat(chatClient: chatClient, utils: utils)
+
+ // When
+ let messageViewModel = MessageViewModel_Mock(
+ message: message,
+ channel: .mock(
+ cid: .unique,
+ membership: .mock(id: .unique, language: .spanish)
+ )
+ )
+ let view = testMessageViewContainer(message: message, messageViewModel: messageViewModel)
+ .environment(\.channelTranslationLanguage, .spanish)
+
+ // Then
+ assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
}
// MARK: - private
- func testMessageViewContainer(message: ChatMessage) -> some View {
+ func testMessageViewContainer(
+ message: ChatMessage,
+ channel: ChatChannel? = nil,
+ messageViewModel: MessageViewModel? = nil
+ ) -> some View {
MessageContainerView(
factory: DefaultViewFactory.shared,
- channel: .mockDMChannel(),
+ channel: channel ?? .mockDMChannel(),
message: message,
width: defaultScreenSize.width,
showsAllInfo: true,
@@ -492,8 +604,17 @@ class MessageContainerView_Tests: StreamChatTestCase {
isLast: false,
scrolledId: .constant(nil),
quotedMessage: .constant(nil),
- onLongPress: { _ in }
+ onLongPress: { _ in },
+ viewModel: messageViewModel ?? MessageViewModel(message: message, channel: channel)
)
.frame(width: 375, height: 200)
}
}
+
+class MessageViewModel_Mock: MessageViewModel {
+ var mockOriginalTextShown: Bool = false
+
+ override var originalTextShown: Bool {
+ mockOriginalTextShown
+ }
+}
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageReadIndicatorView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageReadIndicatorView_Tests.swift
index 9d709e433..4773d5662 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageReadIndicatorView_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageReadIndicatorView_Tests.swift
@@ -72,6 +72,32 @@ class MessageReadIndicatorView_Tests: StreamChatTestCase {
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
}
+ func test_messageReadIndicatorView_snapshotSyncing() {
+ // Given
+ let view = MessageReadIndicatorView(
+ readUsers: [],
+ showReadCount: false,
+ localState: .syncing
+ )
+ .frame(width: 50, height: 16)
+
+ // Then
+ assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
+ }
+
+ func test_messageReadIndicatorView_snapshotSyncing_whenShowReadCount() {
+ // Given
+ let view = MessageReadIndicatorView(
+ readUsers: [.mock(id: .unique)],
+ showReadCount: true,
+ localState: .syncing
+ )
+ .frame(width: 50, height: 16)
+
+ // Then
+ assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
+ }
+
func test_messageReadIndicatorView_snapshotMessageFailed() {
// Given
let view = MessageReadIndicatorView(
@@ -84,4 +110,17 @@ class MessageReadIndicatorView_Tests: StreamChatTestCase {
// Then
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
}
+
+ func test_messageReadIndicatorView_snapshotMessageEditingFailed() {
+ // Given
+ let view = MessageReadIndicatorView(
+ readUsers: [],
+ showReadCount: false,
+ localState: .syncingFailed
+ )
+ .frame(width: 50, height: 16)
+
+ // Then
+ assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
+ }
}
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ReactionsOverlayView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ReactionsOverlayView_Tests.swift
index 03c4f6dbd..701ec6ee2 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannel/ReactionsOverlayView_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/ReactionsOverlayView_Tests.swift
@@ -136,7 +136,7 @@ class ReactionsOverlayView_Tests: StreamChatTestCase {
let view = VerticallyCenteredView {
ReactionsOverlayView(
factory: DefaultViewFactory.shared,
- channel: .mockDMChannel(ownCapabilities: [.sendMessage, .uploadFile, .pinMessage]),
+ channel: .mockDMChannel(ownCapabilities: [.sendMessage, .uploadFile, .pinMessage, .readEvents]),
currentSnapshot: self.overlayImage,
messageDisplayInfo: messageDisplayInfo,
onBackgroundTap: {},
@@ -241,15 +241,17 @@ class ReactionsOverlayView_Tests: StreamChatTestCase {
contentWidth: self.messageDisplayInfo.contentWidth,
isFirst: true
)
+ let channel = ChatChannel.mock(cid: .unique, membership: .mock(id: "test", language: .portuguese))
let view = VerticallyCenteredView {
ReactionsOverlayView(
factory: DefaultViewFactory.shared,
- channel: .mock(cid: .unique, membership: .mock(id: "test", language: .portuguese)),
+ channel: channel,
currentSnapshot: self.overlayImage,
messageDisplayInfo: messageDisplayInfo,
onBackgroundTap: {},
onActionExecuted: { _ in }
)
+ .environment(\.messageViewModel, MessageViewModel(message: testMessage, channel: channel))
}
// Then
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerView_editingFailed_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerView_editingFailed_snapshot.1.png
new file mode 100644
index 000000000..0ed7c0c69
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerView_editingFailed_snapshot.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerView_sendingFailed_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerView_sendingFailed_snapshot.1.png
new file mode 100644
index 000000000..0ed7c0c69
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerView_sendingFailed_snapshot.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_participant_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_participant_snapshot.1.png
new file mode 100644
index 000000000..c16e211b9
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_participant_snapshot.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_showOriginalTranslatedButtonDisabled_translatedTextShown_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_showOriginalTranslatedButtonDisabled_translatedTextShown_snapshot.1.png
new file mode 100644
index 000000000..418bef28c
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_showOriginalTranslatedButtonDisabled_translatedTextShown_snapshot.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_showOriginalTranslatedButtonEnabled_originalTextShown_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_showOriginalTranslatedButtonEnabled_originalTextShown_snapshot.1.png
new file mode 100644
index 000000000..3bfa6c890
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_showOriginalTranslatedButtonEnabled_originalTextShown_snapshot.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_showOriginalTranslatedButtonEnabled_translatedTextShown_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_showOriginalTranslatedButtonEnabled_translatedTextShown_snapshot.1.png
new file mode 100644
index 000000000..afbc90ca5
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_showOriginalTranslatedButtonEnabled_translatedTextShown_snapshot.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageEditingFailed.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageEditingFailed.1.png
new file mode 100644
index 000000000..f210a1ecd
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotMessageEditingFailed.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotSyncing.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotSyncing.1.png
new file mode 100644
index 000000000..567c11648
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotSyncing.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotSyncing_whenShowReadCount.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotSyncing_whenShowReadCount.1.png
new file mode 100644
index 000000000..567c11648
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageReadIndicatorView_Tests/test_messageReadIndicatorView_snapshotSyncing_whenShowReadCount.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/Utils/MessageRelativeDateFormatter_Tests.swift b/StreamChatSwiftUITests/Tests/Utils/MessageRelativeDateFormatter_Tests.swift
new file mode 100644
index 000000000..af8412a4e
--- /dev/null
+++ b/StreamChatSwiftUITests/Tests/Utils/MessageRelativeDateFormatter_Tests.swift
@@ -0,0 +1,63 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+@testable import StreamChat
+@testable import StreamChatSwiftUI
+import XCTest
+
+final class MessageRelativeDateFormatter_Tests: StreamChatTestCase {
+ private var formatter: MessageRelativeDateFormatter!
+
+ override func setUp() {
+ super.setUp()
+ formatter = MessageRelativeDateFormatter()
+ formatter.locale = Locale(identifier: "en_UK")
+ formatter.todayFormatter.locale = Locale(identifier: "en_UK")
+ formatter.yesterdayFormatter.locale = Locale(identifier: "en_UK")
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ formatter = nil
+ }
+
+ func test_showingTimeOnly() throws {
+ let date = try XCTUnwrap(Calendar.current.date(bySettingHour: 1, minute: 2, second: 3, of: Date()))
+ let result = formatter.string(from: date)
+ let expected = formatter.todayFormatter.string(from: date)
+ XCTAssertEqual(expected, result)
+ XCTAssertEqual("01:02", result)
+ }
+
+ func test_showingYesterday() throws {
+ let date = try XCTUnwrap(Calendar.current.date(byAdding: .day, value: -1, to: Date()))
+ let result = formatter.string(from: date)
+ let expected = formatter.yesterdayFormatter.string(from: date)
+ XCTAssertEqual(expected, result)
+ XCTAssertEqual("Yesterday", result)
+ }
+
+ func test_showingWeekday() throws {
+ let date = try XCTUnwrap(Calendar.current.date(byAdding: .day, value: -6, to: Date()))
+ let result = formatter.string(from: date)
+ let expected = formatter.weekdayFormatter.string(from: date)
+ XCTAssertEqual(expected, result)
+ }
+
+ func test_showingShortDate() throws {
+ let components = DateComponents(
+ timeZone: TimeZone(secondsFromGMT: 0),
+ year: 2025,
+ month: 1,
+ day: 15,
+ hour: 3,
+ minute: 4,
+ second: 5
+ )
+ let date = try XCTUnwrap(Calendar.current.date(from: components))
+ let result = formatter.string(from: date)
+ XCTAssertEqual("15/01/2025", result)
+ }
+}
diff --git a/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift b/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift
index 8387b3faa..4a1aa38f5 100644
--- a/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift
@@ -982,7 +982,22 @@ class ViewFactory_Tests: StreamChatTestCase {
)
// Then
- XCTAssert(view is GalleryView)
+ XCTAssert(view is GalleryView)
+ }
+
+ func test_viewFactory_makeGalleryHeaderView() {
+ // Given
+ let viewFactory = DefaultViewFactory.shared
+
+ // When
+ let view = viewFactory.makeGalleryHeaderView(
+ title: .unique,
+ subtitle: .unique,
+ shown: .constant(true)
+ )
+
+ // Then
+ XCTAssert(view is GalleryHeaderView)
}
func test_viewFactory_makeVideoPlayerView() {
@@ -998,7 +1013,22 @@ class ViewFactory_Tests: StreamChatTestCase {
)
// Then
- XCTAssert(view is VideoPlayerView)
+ XCTAssert(view is VideoPlayerView)
+ }
+
+ func test_viewFactory_makeVideoPlayerHeaderView() {
+ // Given
+ let viewFactory = DefaultViewFactory.shared
+
+ // When
+ let view = viewFactory.makeVideoPlayerHeaderView(
+ title: .unique,
+ subtitle: .unique,
+ shown: .constant(true)
+ )
+
+ // Then
+ XCTAssert(view is GalleryHeaderView)
}
}
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index cc59dc920..b5d6b1f12 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -7,7 +7,7 @@ require 'xcodeproj'
import 'Sonarfile'
import 'Allurefile'
-xcode_version = ENV['XCODE_VERSION'] || '16.2'
+xcode_version = ENV['XCODE_VERSION'] || '16.3'
xcode_project = 'StreamChatSwiftUI.xcodeproj'
sdk_names = ['StreamChatSwiftUI']
github_repo = ENV['GITHUB_REPOSITORY'] || 'GetStream/stream-chat-swiftui'
@@ -23,7 +23,7 @@ before_all do |lane|
if is_ci
setup_ci
setup_git_config
- xcversion(version: xcode_version) unless [:sonar_upload, :allure_launch, :allure_upload, :copyright, :pod_lint].include?(lane)
+ select_xcode(version: xcode_version) unless [:sonar_upload, :allure_launch, :allure_upload, :copyright, :pod_lint].include?(lane)
end
end
@@ -217,15 +217,31 @@ lane :match_me do |options|
end
desc 'Builds the latest version of Demo app and uploads it to TestFlight'
-lane :swiftui_testflight_build do
+lane :swiftui_testflight_build do |options|
+ is_manual_upload = is_localhost || !options[:configuration].to_s.empty?
+ configuration = options[:configuration].to_s.empty? ? 'Release' : options[:configuration]
+
match_me
+
+ sdk_version = get_sdk_version_from_environment
+ app_version =
+ if is_manual_upload
+ major, minor, _patch = sdk_version.split('.').map(&:to_i)
+ minor += 1
+ "#{major}.#{minor}.0"
+ else
+ sdk_version
+ end
+ UI.important("[TestFlight] Uploading DemoApp version: #{app_version}")
+
testflight_build(
api_key: appstore_api_key,
xcode_project: xcode_project,
sdk_target: 'StreamChatSwiftUI',
+ app_version: app_version,
app_target: 'DemoAppSwiftUI',
app_identifier: 'io.getstream.iOS.DemoAppSwiftUI',
- app_version: File.read(swift_environment_path).match(/String\s+=\s+"([\d.]+)"/)[1]
+ configuration: configuration
)
end
@@ -320,29 +336,24 @@ lane :test_e2e_mock do |options|
number_of_retries: 3
}
- if is_localhost
- scan(scan_options)
- else
- parallelize_tests_on_ci(scan: scan_options, batch: options[:batch], cron: options[:cron])
+ if ENV['MATRIX_SIZE'] && options[:batch]
+ products_dir = File.expand_path("../#{derived_data_path}/Build/Products")
+ xctestrun = Dir.glob(File.expand_path("#{products_dir}/*.xctestrun")).first
+ tests = retrieve_xctest_names(xctestrun: xctestrun).values.flatten
+ slice_size = (tests.size / ENV['MATRIX_SIZE'].to_f).ceil
+ only_testing = []
+ tests.each_slice(slice_size) { |test| only_testing << test }
+ only_testing_batch = only_testing[options[:batch].to_i]
+ scan_options[:only_testing] = only_testing_batch
+ UI.important("Tests in total: #{only_testing.flatten.size}. Running #{only_testing_batch.size} of them ⌛️")
end
-end
-
-private_lane :parallelize_tests_on_ci do |options|
- products_dir = File.expand_path("../#{derived_data_path}/Build/Products")
- xctestrun = Dir.glob(File.expand_path("#{products_dir}/*.xctestrun")).first
- tests = retrieve_xctest_names(xctestrun: xctestrun).values.flatten
- slice_size = options[:cron] ? tests.size : (tests.size / ENV['MATRIX_SIZE'].to_f).ceil
- only_testing = []
- tests.each_slice(slice_size) { |test| only_testing << test }
- only_testing_batch = only_testing[options[:batch].to_i]
begin
- UI.success("Tests in total: #{only_testing.flatten.size}. Running #{only_testing_batch.size} of them ⌛️")
- scan(options[:scan].merge(only_testing: only_testing_batch))
+ scan(scan_options)
rescue StandardError
failed_tests = retreive_failed_tests
UI.important("Re-running #{failed_tests.size} failed tests ⌛️")
- scan(options[:scan].merge(only_testing: failed_tests))
+ scan(scan_options.merge(only_testing: failed_tests))
end
end
diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile
index 09a35c441..3fc4f8457 100644
--- a/fastlane/Pluginfile
+++ b/fastlane/Pluginfile
@@ -5,4 +5,4 @@
gem 'fastlane-plugin-versioning'
gem 'fastlane-plugin-sonarcloud_metric_kit'
gem 'fastlane-plugin-create_xcframework'
-gem 'fastlane-plugin-stream_actions', '0.3.77'
+gem 'fastlane-plugin-stream_actions', '0.3.79'