Skip to content

Commit e888591

Browse files
[CHAT-530] Message reminders (#169)
* Added the support for message reminders feature * `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 * chore: update CONTRIBUTING.md and add Makefile for development workflow * Add Docker-based development commands to CONTRIBUTING.md * Create Makefile with development, testing, and Docker-related targets * Include support for customizable Ruby version and Stream Chat URL * Add convenience commands for linting, testing, and type checking * fixed linting errorsç * merge "master" into 'feature/snooze_message_reminder' * fixed failing specs * fixed duplicate method definition * chore: fixed failing specs * chore: lint fixes * chore: fixed srb linting errors
1 parent 79aa72a commit e888591

File tree

7 files changed

+248
-4
lines changed

7 files changed

+248
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,4 +350,4 @@ before continuing with v3.0.0 of this library.
350350
- Added `client.search`
351351
- Added `client.update_users_partial`
352352
- Added `client.update_user_partial`
353-
- Added `client.reactivate_user`
353+
- Added `client.reactivate_user`

CONTRIBUTING.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# :recycle: Contributing
32

43
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.
@@ -63,6 +62,34 @@ Recommended settings:
6362
}
6463
```
6564

65+
For Docker-based development, you can use:
66+
67+
```shell
68+
$ make lint_with_docker # Run linters in Docker
69+
$ make lint-fix_with_docker # Fix linting issues in Docker
70+
$ make test_with_docker # Run tests in Docker
71+
$ make check_with_docker # Run both linters and tests in Docker
72+
$ make sorbet_with_docker # Run Sorbet type checker in Docker
73+
```
74+
75+
You can customize the Ruby version used in Docker by setting the RUBY_VERSION variable:
76+
77+
```shell
78+
$ RUBY_VERSION=3.1 make test_with_docker
79+
```
80+
81+
By default, the API client connects to the production Stream Chat API. You can override this by setting the STREAM_CHAT_URL environment variable:
82+
83+
```shell
84+
$ STREAM_CHAT_URL=http://localhost:3030 make test
85+
```
86+
87+
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:
88+
89+
```shell
90+
$ STREAM_CHAT_URL=http://host.docker.internal:3030 make test_with_docker
91+
```
92+
6693
### Commit message convention
6794

6895
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.

Makefile

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
STREAM_KEY ?= NOT_EXIST
2+
STREAM_SECRET ?= NOT_EXIST
3+
RUBY_VERSION ?= 3.0
4+
STREAM_CHAT_URL ?= https://chat.stream-io-api.com
5+
6+
# These targets are not files
7+
.PHONY: help check test lint lint-fix test_with_docker lint_with_docker lint-fix_with_docker
8+
9+
help: ## Display this help message
10+
@echo "Please use \`make <target>\` where <target> is one of"
11+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; \
12+
{printf "\033[36m%-40s\033[0m %s\n", $$1, $$2}'
13+
14+
lint: ## Run linters
15+
bundle exec rubocop
16+
17+
lint-fix: ## Fix linting issues
18+
bundle exec rubocop -a
19+
20+
test: ## Run tests
21+
STREAM_KEY=$(STREAM_KEY) STREAM_SECRET=$(STREAM_SECRET) bundle exec rspec
22+
23+
check: lint test ## Run linters + tests
24+
25+
console: ## Start a console with the gem loaded
26+
bundle exec rake console
27+
28+
lint_with_docker: ## Run linters in Docker (set RUBY_VERSION to change Ruby version)
29+
docker run -t -i -w /code -v $(PWD):/code ruby:$(RUBY_VERSION) sh -c "gem install bundler && bundle install && bundle exec rubocop"
30+
31+
lint-fix_with_docker: ## Fix linting issues in Docker (set RUBY_VERSION to change Ruby version)
32+
docker run -t -i -w /code -v $(PWD):/code ruby:$(RUBY_VERSION) sh -c "gem install bundler && bundle install && bundle exec rubocop -a"
33+
34+
test_with_docker: ## Run tests in Docker (set RUBY_VERSION to change Ruby version)
35+
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"
36+
37+
check_with_docker: lint_with_docker test_with_docker ## Run linters + tests in Docker (set RUBY_VERSION to change Ruby version)
38+
39+
sorbet: ## Run Sorbet type checker
40+
bundle exec srb tc
41+
42+
sorbet_with_docker: ## Run Sorbet type checker in Docker (set RUBY_VERSION to change Ruby version)
43+
docker run -t -i -w /code -v $(PWD):/code ruby:$(RUBY_VERSION) sh -c "gem install bundler && bundle install && bundle exec srb tc"
44+
45+
coverage: ## Generate test coverage report
46+
COVERAGE=true bundle exec rspec
47+
@echo "Coverage report available at ./coverage/index.html"
48+
49+
reviewdog: ## Run reviewdog for CI
50+
bundle exec rubocop --format json | reviewdog -f=rubocop -name=rubocop -reporter=github-pr-review

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,29 @@ deleted_message = client.delete_message(m1['message']['id'])
132132

133133
```
134134

135+
### Reminders
136+
137+
```ruby
138+
# Create a reminder for a message
139+
reminder = client.create_reminder(m1['message']['id'], 'bob-1', DateTime.now + 1)
140+
141+
# Create a reminder without a notification time (just mark for later)
142+
reminder = client.create_reminder(m1['message']['id'], 'bob-1')
143+
144+
# Update a reminder
145+
updated_reminder = client.update_reminder(m1['message']['id'], 'bob-1', DateTime.now + 2)
146+
147+
# Delete a reminder
148+
client.delete_reminder(m1['message']['id'], 'bob-1')
149+
150+
# Query reminders for a user
151+
reminders = client.query_reminders('bob-1')
152+
153+
# Query reminders with filters
154+
filter = { 'channel_cid' => 'messaging:bob-and-jane' }
155+
reminders = client.query_reminders('bob-1', filter)
156+
```
157+
135158
### Devices
136159

137160
```ruby

lib/stream-chat/client.rb

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require 'faraday/net_http_persistent'
88
require 'jwt'
99
require 'time'
10+
require 'date'
1011
require 'sorbet-runtime'
1112
require 'stream-chat/channel'
1213
require 'stream-chat/errors'
@@ -688,7 +689,7 @@ def delete_channels(cids, hard_delete: false)
688689
# Revoke tokens for an application issued since the given date.
689690
sig { params(before: T.any(DateTime, String)).returns(StreamChat::StreamResponse) }
690691
def revoke_tokens(before)
691-
before = T.cast(before, DateTime).rfc3339 if before.instance_of?(DateTime)
692+
before = before.rfc3339 if before.instance_of?(DateTime)
692693
update_app_settings({ 'revoke_tokens_issued_before' => before })
693694
end
694695

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

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

943+
# Creates a reminder for a message.
944+
# @param message_id [String] The ID of the message to create a reminder for
945+
# @param user_id [String] The ID of the user creating the reminder
946+
# @param remind_at [DateTime, nil] When to remind the user (optional)
947+
# @return [StreamChat::StreamResponse] API response
948+
sig { params(message_id: String, user_id: String, remind_at: T.nilable(DateTime)).returns(StreamChat::StreamResponse) }
949+
def create_reminder(message_id, user_id, remind_at = nil)
950+
data = { user_id: user_id }
951+
data[:remind_at] = remind_at.rfc3339 if remind_at.instance_of?(DateTime)
952+
post("messages/#{message_id}/reminders", data: data)
953+
end
954+
955+
# Updates a reminder for a message.
956+
# @param message_id [String] The ID of the message with the reminder
957+
# @param user_id [String] The ID of the user who owns the reminder
958+
# @param remind_at [DateTime, nil] When to remind the user (optional)
959+
# @return [StreamChat::StreamResponse] API response
960+
sig { params(message_id: String, user_id: String, remind_at: T.nilable(DateTime)).returns(StreamChat::StreamResponse) }
961+
def update_reminder(message_id, user_id, remind_at = nil)
962+
data = { user_id: user_id }
963+
data[:remind_at] = remind_at.rfc3339 if remind_at
964+
patch("messages/#{message_id}/reminders", data: data)
965+
end
966+
967+
# Deletes a reminder for a message.
968+
# @param message_id [String] The ID of the message with the reminder
969+
# @param user_id [String] The ID of the user who owns the reminder
970+
# @return [StreamChat::StreamResponse] API response
971+
sig { params(message_id: String, user_id: String).returns(StreamChat::StreamResponse) }
972+
def delete_reminder(message_id, user_id)
973+
delete("messages/#{message_id}/reminders", params: { user_id: user_id })
974+
end
975+
976+
# Queries reminders based on filter conditions.
977+
# @param user_id [String] The ID of the user whose reminders to query
978+
# @param filter_conditions [Hash] Conditions to filter reminders
979+
# @param sort [Array<Hash>, nil] Sort parameters (default: [{ field: 'remind_at', direction: 1 }])
980+
# @param options [Hash] Additional query options like limit, offset
981+
# @return [StreamChat::StreamResponse] API response with reminders
982+
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) }
983+
def query_reminders(user_id, filter_conditions = {}, sort: nil, **options)
984+
params = options.merge({
985+
filter_conditions: filter_conditions,
986+
sort: sort || [{ field: 'remind_at', direction: 1 }],
987+
user_id: user_id
988+
})
989+
post('reminders/query', data: params)
990+
end
991+
942992
private
943993

944994
sig { returns(T::Hash[String, String]) }

spec/channel_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ def loop_times(times)
185185
end
186186

187187
it 'can mark messages as read' do
188+
@channel.add_members([@random_user[:id]])
188189
response = @channel.mark_read(@random_user[:id])
189190
expect(response).to include 'event'
190191
expect(response['event']['type']).to eq 'message.read'

spec/client_spec.rb

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,4 +1020,97 @@ def loop_times(times)
10201020
expect(response['threads'].length).to be >= 1
10211021
end
10221022
end
1023+
1024+
describe 'reminders' do
1025+
before do
1026+
@client = StreamChat::Client.from_env
1027+
@channel_id = SecureRandom.uuid
1028+
@channel = @client.channel('messaging', channel_id: @channel_id)
1029+
@channel.create('john')
1030+
@channel.update_partial({ config_overrides: { user_message_reminders: true } })
1031+
@message = @channel.send_message({ 'text' => 'Hello world' }, 'john')
1032+
@message_id = @message['message']['id']
1033+
@user_id = 'john'
1034+
end
1035+
1036+
describe 'create_reminder' do
1037+
it 'create reminder' do
1038+
remind_at = DateTime.now + 1
1039+
response = @client.create_reminder(@message_id, @user_id, remind_at)
1040+
1041+
expect(response).to include('reminder')
1042+
expect(response['reminder']).to include('message_id', 'user_id', 'remind_at')
1043+
expect(response['reminder']['message_id']).to eq(@message_id)
1044+
expect(response['reminder']['user_id']).to eq(@user_id)
1045+
end
1046+
1047+
it 'create reminder without remind_at' do
1048+
response = @client.create_reminder(@message_id, @user_id)
1049+
1050+
expect(response).to include('reminder')
1051+
expect(response['reminder']).to include('message_id', 'user_id')
1052+
expect(response['reminder']['message_id']).to eq(@message_id)
1053+
expect(response['reminder']['user_id']).to eq(@user_id)
1054+
expect(response['reminder']['remind_at']).to be_nil
1055+
end
1056+
end
1057+
1058+
describe 'update_reminder' do
1059+
before do
1060+
@client.create_reminder(@message_id, @user_id)
1061+
end
1062+
1063+
it 'update reminder' do
1064+
new_remind_at = DateTime.now + 2
1065+
response = @client.update_reminder(@message_id, @user_id, new_remind_at)
1066+
1067+
expect(response).to include('reminder')
1068+
expect(response['reminder']).to include('message_id', 'user_id', 'remind_at')
1069+
expect(response['reminder']['message_id']).to eq(@message_id)
1070+
expect(response['reminder']['user_id']).to eq(@user_id)
1071+
expect(DateTime.parse(response['reminder']['remind_at'])).to be_within(1).of(new_remind_at)
1072+
end
1073+
end
1074+
1075+
describe 'delete_reminder' do
1076+
before do
1077+
@client.create_reminder(@message_id, @user_id)
1078+
end
1079+
1080+
it 'delete reminder' do
1081+
response = @client.delete_reminder(@message_id, @user_id)
1082+
expect(response).to be_a(Hash)
1083+
end
1084+
end
1085+
1086+
describe 'query_reminders' do
1087+
before do
1088+
@reminder = @client.create_reminder(@message_id, @user_id)
1089+
end
1090+
1091+
it 'query reminders' do
1092+
# Query reminders for the user
1093+
response = @client.query_reminders(@user_id)
1094+
1095+
expect(response).to include('reminders')
1096+
expect(response['reminders']).to be_an(Array)
1097+
expect(response['reminders'].length).to be >= 1
1098+
end
1099+
1100+
it 'query reminders with channel filter' do
1101+
# Query reminders for the user in a specific channel
1102+
filter = { 'channel_cid' => @channel.cid }
1103+
response = @client.query_reminders(@user_id, filter)
1104+
1105+
expect(response).to include('reminders')
1106+
expect(response['reminders']).to be_an(Array)
1107+
expect(response['reminders'].length).to be >= 1
1108+
1109+
# All reminders should have a channel_cid
1110+
response['reminders'].each do |reminder|
1111+
expect(reminder).to include('channel_cid')
1112+
end
1113+
end
1114+
end
1115+
end
10231116
end

0 commit comments

Comments
 (0)