Skip to content

[CHAT-530] Feature/snooze message reminder #152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## [Unreleased]

### Features

* Added support for message reminders:
* `create_reminder`: Create a reminder for a message
* `update_reminder`: Update an existing reminder
* `delete_reminder`: Delete a reminder
* `query_reminders`: Query reminders with filtering options

## [3.10.0](https://github.com/GetStream/stream-chat-ruby/compare/v3.9.0...v3.10.0) (2025-02-24)

## [3.9.0](https://github.com/GetStream/stream-chat-ruby/compare/v3.7.0...v3.9.0) (2025-02-11)
Expand Down
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
50 changes: 50 additions & 0 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 @@ -900,6 +901,55 @@ def list_imports(options)
get('imports', params: options)
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] = T.cast(remind_at, DateTime).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
98 changes: 98 additions & 0 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -846,4 +846,102 @@ def loop_times(times)
end
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')
@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
remind_at = DateTime.now + 1
@client.create_reminder(@message_id, @user_id, remind_at)
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

# Find our reminder
reminder = response['reminders'].find { |r| r['message_id'] == @message_id }
expect(reminder).not_to be_nil
expect(reminder['user_id']).to eq(@user_id)
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
Loading