Skip to content
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,4 +350,4 @@ before continuing with v3.0.0 of this library.
- Added `client.search`
- Added `client.update_users_partial`
- Added `client.update_user_partial`
- Added `client.reactivate_user`
- Added `client.reactivate_user`
29 changes: 28 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# :recycle: Contributing

We welcome code changes that improve this library or fix a problem, please make sure to follow all best practices and add tests if applicable before submitting a Pull Request on Github. We are very happy to merge your code in the official repository. Make sure to sign our [Contributor License Agreement (CLA)](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) first. See our license file for more details.
Expand Down Expand Up @@ -63,6 +62,34 @@ Recommended settings:
}
```

For Docker-based development, you can use:

```shell
$ make lint_with_docker # Run linters in Docker
$ make lint-fix_with_docker # Fix linting issues in Docker
$ make test_with_docker # Run tests in Docker
$ make check_with_docker # Run both linters and tests in Docker
$ make sorbet_with_docker # Run Sorbet type checker in Docker
```

You can customize the Ruby version used in Docker by setting the RUBY_VERSION variable:

```shell
$ RUBY_VERSION=3.1 make test_with_docker
```

By default, the API client connects to the production Stream Chat API. You can override this by setting the STREAM_CHAT_URL environment variable:

```shell
$ STREAM_CHAT_URL=http://localhost:3030 make test
```

When running tests in Docker, the `test_with_docker` command automatically sets up networking to allow the Docker container to access services running on your host machine via `host.docker.internal`. This is particularly useful for connecting to a local Stream Chat server:

```shell
$ STREAM_CHAT_URL=http://host.docker.internal:3030 make test_with_docker
```

### Commit message convention

This repository follows a commit message convention in order to automatically generate the [CHANGELOG](./CHANGELOG.md). Make sure you follow the rules of [conventional commits](https://www.conventionalcommits.org/) when opening a pull request.
Expand Down
50 changes: 50 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
STREAM_KEY ?= NOT_EXIST
STREAM_SECRET ?= NOT_EXIST
RUBY_VERSION ?= 3.0
STREAM_CHAT_URL ?= https://chat.stream-io-api.com

# These targets are not files
.PHONY: help check test lint lint-fix test_with_docker lint_with_docker lint-fix_with_docker

help: ## Display this help message
@echo "Please use \`make <target>\` where <target> is one of"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; \
{printf "\033[36m%-40s\033[0m %s\n", $$1, $$2}'

lint: ## Run linters
bundle exec rubocop

lint-fix: ## Fix linting issues
bundle exec rubocop -a

test: ## Run tests
STREAM_KEY=$(STREAM_KEY) STREAM_SECRET=$(STREAM_SECRET) bundle exec rspec

check: lint test ## Run linters + tests

console: ## Start a console with the gem loaded
bundle exec rake console

lint_with_docker: ## Run linters in Docker (set RUBY_VERSION to change Ruby version)
docker run -t -i -w /code -v $(PWD):/code ruby:$(RUBY_VERSION) sh -c "gem install bundler && bundle install && bundle exec rubocop"

lint-fix_with_docker: ## Fix linting issues in Docker (set RUBY_VERSION to change Ruby version)
docker run -t -i -w /code -v $(PWD):/code ruby:$(RUBY_VERSION) sh -c "gem install bundler && bundle install && bundle exec rubocop -a"

test_with_docker: ## Run tests in Docker (set RUBY_VERSION to change Ruby version)
docker run -t -i -w /code -v $(PWD):/code --add-host=host.docker.internal:host-gateway -e STREAM_KEY=$(STREAM_KEY) -e STREAM_SECRET=$(STREAM_SECRET) -e "STREAM_CHAT_URL=http://host.docker.internal:3030" ruby:$(RUBY_VERSION) sh -c "gem install bundler && bundle install && bundle exec rspec"

check_with_docker: lint_with_docker test_with_docker ## Run linters + tests in Docker (set RUBY_VERSION to change Ruby version)

sorbet: ## Run Sorbet type checker
bundle exec srb tc

sorbet_with_docker: ## Run Sorbet type checker in Docker (set RUBY_VERSION to change Ruby version)
docker run -t -i -w /code -v $(PWD):/code ruby:$(RUBY_VERSION) sh -c "gem install bundler && bundle install && bundle exec srb tc"

coverage: ## Generate test coverage report
COVERAGE=true bundle exec rspec
@echo "Coverage report available at ./coverage/index.html"

reviewdog: ## Run reviewdog for CI
bundle exec rubocop --format json | reviewdog -f=rubocop -name=rubocop -reporter=github-pr-review
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,29 @@ deleted_message = client.delete_message(m1['message']['id'])

```

### Reminders

```ruby
# Create a reminder for a message
reminder = client.create_reminder(m1['message']['id'], 'bob-1', DateTime.now + 1)

# Create a reminder without a notification time (just mark for later)
reminder = client.create_reminder(m1['message']['id'], 'bob-1')

# Update a reminder
updated_reminder = client.update_reminder(m1['message']['id'], 'bob-1', DateTime.now + 2)

# Delete a reminder
client.delete_reminder(m1['message']['id'], 'bob-1')

# Query reminders for a user
reminders = client.query_reminders('bob-1')

# Query reminders with filters
filter = { 'channel_cid' => 'messaging:bob-and-jane' }
reminders = client.query_reminders('bob-1', filter)
```

### Devices

```ruby
Expand Down
54 changes: 52 additions & 2 deletions lib/stream-chat/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require 'faraday/net_http_persistent'
require 'jwt'
require 'time'
require 'date'
require 'sorbet-runtime'
require 'stream-chat/channel'
require 'stream-chat/errors'
Expand Down Expand Up @@ -688,7 +689,7 @@ def delete_channels(cids, hard_delete: false)
# Revoke tokens for an application issued since the given date.
sig { params(before: T.any(DateTime, String)).returns(StreamChat::StreamResponse) }
def revoke_tokens(before)
before = T.cast(before, DateTime).rfc3339 if before.instance_of?(DateTime)
before = before.rfc3339 if before.instance_of?(DateTime)
update_app_settings({ 'revoke_tokens_issued_before' => before })
end

Expand All @@ -701,7 +702,7 @@ def revoke_user_token(user_id, before)
# Revoke tokens for users issued since.
sig { params(user_ids: T::Array[String], before: T.any(DateTime, String)).returns(StreamChat::StreamResponse) }
def revoke_users_token(user_ids, before)
before = T.cast(before, DateTime).rfc3339 if before.instance_of?(DateTime)
before = before.rfc3339 if before.instance_of?(DateTime)

updates = []
user_ids.map do |user_id|
Expand Down Expand Up @@ -939,6 +940,55 @@ def query_threads(filter, sort: nil, **options)
post('threads', data: params)
end

# Creates a reminder for a message.
# @param message_id [String] The ID of the message to create a reminder for
# @param user_id [String] The ID of the user creating the reminder
# @param remind_at [DateTime, nil] When to remind the user (optional)
# @return [StreamChat::StreamResponse] API response
sig { params(message_id: String, user_id: String, remind_at: T.nilable(DateTime)).returns(StreamChat::StreamResponse) }
def create_reminder(message_id, user_id, remind_at = nil)
data = { user_id: user_id }
data[:remind_at] = remind_at.rfc3339 if remind_at.instance_of?(DateTime)
post("messages/#{message_id}/reminders", data: data)
end

# Updates a reminder for a message.
# @param message_id [String] The ID of the message with the reminder
# @param user_id [String] The ID of the user who owns the reminder
# @param remind_at [DateTime, nil] When to remind the user (optional)
# @return [StreamChat::StreamResponse] API response
sig { params(message_id: String, user_id: String, remind_at: T.nilable(DateTime)).returns(StreamChat::StreamResponse) }
def update_reminder(message_id, user_id, remind_at = nil)
data = { user_id: user_id }
data[:remind_at] = remind_at.rfc3339 if remind_at
patch("messages/#{message_id}/reminders", data: data)
end

# Deletes a reminder for a message.
# @param message_id [String] The ID of the message with the reminder
# @param user_id [String] The ID of the user who owns the reminder
# @return [StreamChat::StreamResponse] API response
sig { params(message_id: String, user_id: String).returns(StreamChat::StreamResponse) }
def delete_reminder(message_id, user_id)
delete("messages/#{message_id}/reminders", params: { user_id: user_id })
end

# Queries reminders based on filter conditions.
# @param user_id [String] The ID of the user whose reminders to query
# @param filter_conditions [Hash] Conditions to filter reminders
# @param sort [Array<Hash>, nil] Sort parameters (default: [{ field: 'remind_at', direction: 1 }])
# @param options [Hash] Additional query options like limit, offset
# @return [StreamChat::StreamResponse] API response with reminders
sig { params(user_id: String, filter_conditions: T::Hash[T.untyped, T.untyped], sort: T.nilable(T::Array[T::Hash[T.untyped, T.untyped]]), options: T.untyped).returns(StreamChat::StreamResponse) }
def query_reminders(user_id, filter_conditions = {}, sort: nil, **options)
params = options.merge({
filter_conditions: filter_conditions,
sort: sort || [{ field: 'remind_at', direction: 1 }],
user_id: user_id
})
post('reminders/query', data: params)
end

private

sig { returns(T::Hash[String, String]) }
Expand Down
1 change: 1 addition & 0 deletions spec/channel_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ def loop_times(times)
end

it 'can mark messages as read' do
@channel.add_members([@random_user[:id]])
response = @channel.mark_read(@random_user[:id])
expect(response).to include 'event'
expect(response['event']['type']).to eq 'message.read'
Expand Down
93 changes: 93 additions & 0 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1020,4 +1020,97 @@ def loop_times(times)
expect(response['threads'].length).to be >= 1
end
end

describe 'reminders' do
before do
@client = StreamChat::Client.from_env
@channel_id = SecureRandom.uuid
@channel = @client.channel('messaging', channel_id: @channel_id)
@channel.create('john')
@channel.update_partial({ config_overrides: { user_message_reminders: true } })
@message = @channel.send_message({ 'text' => 'Hello world' }, 'john')
@message_id = @message['message']['id']
@user_id = 'john'
end

describe 'create_reminder' do
it 'create reminder' do
remind_at = DateTime.now + 1
response = @client.create_reminder(@message_id, @user_id, remind_at)

expect(response).to include('reminder')
expect(response['reminder']).to include('message_id', 'user_id', 'remind_at')
expect(response['reminder']['message_id']).to eq(@message_id)
expect(response['reminder']['user_id']).to eq(@user_id)
end

it 'create reminder without remind_at' do
response = @client.create_reminder(@message_id, @user_id)

expect(response).to include('reminder')
expect(response['reminder']).to include('message_id', 'user_id')
expect(response['reminder']['message_id']).to eq(@message_id)
expect(response['reminder']['user_id']).to eq(@user_id)
expect(response['reminder']['remind_at']).to be_nil
end
end

describe 'update_reminder' do
before do
@client.create_reminder(@message_id, @user_id)
end

it 'update reminder' do
new_remind_at = DateTime.now + 2
response = @client.update_reminder(@message_id, @user_id, new_remind_at)

expect(response).to include('reminder')
expect(response['reminder']).to include('message_id', 'user_id', 'remind_at')
expect(response['reminder']['message_id']).to eq(@message_id)
expect(response['reminder']['user_id']).to eq(@user_id)
expect(DateTime.parse(response['reminder']['remind_at'])).to be_within(1).of(new_remind_at)
end
end

describe 'delete_reminder' do
before do
@client.create_reminder(@message_id, @user_id)
end

it 'delete reminder' do
response = @client.delete_reminder(@message_id, @user_id)
expect(response).to be_a(Hash)
end
end

describe 'query_reminders' do
before do
@reminder = @client.create_reminder(@message_id, @user_id)
end

it 'query reminders' do
# Query reminders for the user
response = @client.query_reminders(@user_id)

expect(response).to include('reminders')
expect(response['reminders']).to be_an(Array)
expect(response['reminders'].length).to be >= 1
end

it 'query reminders with channel filter' do
# Query reminders for the user in a specific channel
filter = { 'channel_cid' => @channel.cid }
response = @client.query_reminders(@user_id, filter)

expect(response).to include('reminders')
expect(response['reminders']).to be_an(Array)
expect(response['reminders'].length).to be >= 1

# All reminders should have a channel_cid
response['reminders'].each do |reminder|
expect(reminder).to include('channel_cid')
end
end
end
end
end