From 0b9869f3c99dff0bf4126dec4b2065ef9745351c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Mon, 19 Jan 2026 13:51:14 +0100 Subject: [PATCH 01/15] Add JSON response support for `notifications/tray`. --- app/views/notifications/trays/show.json.jbuilder | 1 + test/controllers/notifications/trays_controller_test.rb | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 app/views/notifications/trays/show.json.jbuilder diff --git a/app/views/notifications/trays/show.json.jbuilder b/app/views/notifications/trays/show.json.jbuilder new file mode 100644 index 000000000..0c2928cef --- /dev/null +++ b/app/views/notifications/trays/show.json.jbuilder @@ -0,0 +1 @@ +json.array! @notifications, partial: "notifications/notification", as: :notification diff --git a/test/controllers/notifications/trays_controller_test.rb b/test/controllers/notifications/trays_controller_test.rb index 07034fff0..d0916e305 100644 --- a/test/controllers/notifications/trays_controller_test.rb +++ b/test/controllers/notifications/trays_controller_test.rb @@ -11,4 +11,13 @@ class Notifications::TraysControllerTest < ActionDispatch::IntegrationTest assert_response :success assert_select "div", text: /Layout is broken/ end + + test "show as JSON" do + expected_ids = users(:kevin).notifications.unread.ordered.limit(100).pluck(:id) + + get tray_notifications_path(format: :json) + + assert_response :success + assert_equal expected_ids, @response.parsed_body.map { |notification| notification["id"] } + end end From 3dca00a9abd8334ab28d5794f598f00da3e04b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Mon, 19 Jan 2026 14:00:26 +0100 Subject: [PATCH 02/15] Extend notification JSON payload. Add avatar url, board name, and column. --- app/views/notifications/_notification.json.jbuilder | 9 +++++++-- test/controllers/notifications_controller_test.rb | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/views/notifications/_notification.json.jbuilder b/app/views/notifications/_notification.json.jbuilder index d2fadfa1b..2b0cb3549 100644 --- a/app/views/notifications/_notification.json.jbuilder +++ b/app/views/notifications/_notification.json.jbuilder @@ -6,11 +6,16 @@ json.cache! notification do json.partial! "notifications/notification/#{notification.source_type.underscore}/body", notification: notification - json.creator notification.creator, partial: "users/user", as: :user + json.creator do + json.partial! "users/user", user: notification.creator + json.avatar_url user_avatar_url(notification.creator) + end json.card do - json.(notification.card, :id, :title, :status) + json.(notification.card, :id, :number, :title, :status) + json.board_name notification.card.board.name json.url card_url(notification.card) + json.column notification.card.column, partial: "columns/column", as: :column if notification.card.column end json.url notification_url(notification) diff --git a/test/controllers/notifications_controller_test.rb b/test/controllers/notifications_controller_test.rb index 0d0565f24..aa9c283a5 100644 --- a/test/controllers/notifications_controller_test.rb +++ b/test/controllers/notifications_controller_test.rb @@ -22,5 +22,9 @@ class NotificationsControllerTest < ActionDispatch::IntegrationTest assert_not_nil notification["card"] assert_not_nil notification["creator"] assert_not_nil notification["unread_count"] + assert_not_nil notification.dig("creator", "avatar_url") + assert_not_nil notification.dig("card", "number") + assert_not_nil notification.dig("card", "board_name") + assert_not_nil notification.dig("card", "column") end end From af2efe6d76e4d959b0dce5f6986edac87c2f0e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Mon, 19 Jan 2026 16:57:12 +0100 Subject: [PATCH 03/15] Add `avatar_url` to the shared user JSON partial. --- app/views/users/_user.json.jbuilder | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/users/_user.json.jbuilder b/app/views/users/_user.json.jbuilder index 0884f225a..ed05bc67f 100644 --- a/app/views/users/_user.json.jbuilder +++ b/app/views/users/_user.json.jbuilder @@ -5,4 +5,5 @@ json.cache! user do json.created_at user.created_at.utc json.url user_url(user) + json.avatar_url user_avatar_url(user) end From 683547e310e2a9b2b25a7e04939189340563279f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Mon, 19 Jan 2026 17:00:14 +0100 Subject: [PATCH 04/15] Use the existing user partial on notifications. --- app/views/notifications/_notification.json.jbuilder | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/views/notifications/_notification.json.jbuilder b/app/views/notifications/_notification.json.jbuilder index 2b0cb3549..5673097a7 100644 --- a/app/views/notifications/_notification.json.jbuilder +++ b/app/views/notifications/_notification.json.jbuilder @@ -6,10 +6,7 @@ json.cache! notification do json.partial! "notifications/notification/#{notification.source_type.underscore}/body", notification: notification - json.creator do - json.partial! "users/user", user: notification.creator - json.avatar_url user_avatar_url(notification.creator) - end + json.creator notification.creator, partial: "users/user", as: :user json.card do json.(notification.card, :id, :number, :title, :status) From 009e431ad8a1822401561a76cb3f6f64c3f17d3e Mon Sep 17 00:00:00 2001 From: Fernando Olivares Date: Thu, 22 Jan 2026 03:00:17 -0600 Subject: [PATCH 05/15] Add has_attachments to notification card JSON Co-Authored-By: Claude Opus 4.5 --- app/views/notifications/_notification.json.jbuilder | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/notifications/_notification.json.jbuilder b/app/views/notifications/_notification.json.jbuilder index 5673097a7..ac85f837f 100644 --- a/app/views/notifications/_notification.json.jbuilder +++ b/app/views/notifications/_notification.json.jbuilder @@ -11,6 +11,7 @@ json.cache! notification do json.card do json.(notification.card, :id, :number, :title, :status) json.board_name notification.card.board.name + json.has_attachments notification.card.has_attachments? json.url card_url(notification.card) json.column notification.card.column, partial: "columns/column", as: :column if notification.card.column end From 6809804d3a6d952933c549c1ae441c45c14abc82 Mon Sep 17 00:00:00 2001 From: Fernando Olivares Date: Thu, 22 Jan 2026 03:13:31 -0600 Subject: [PATCH 06/15] Add card assignees to notification JSON Includes assignee users in the card object for display in notification UI. Co-Authored-By: Claude Opus 4.5 --- app/views/notifications/_notification.json.jbuilder | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/notifications/_notification.json.jbuilder b/app/views/notifications/_notification.json.jbuilder index ac85f837f..7d269d46d 100644 --- a/app/views/notifications/_notification.json.jbuilder +++ b/app/views/notifications/_notification.json.jbuilder @@ -12,6 +12,7 @@ json.cache! notification do json.(notification.card, :id, :number, :title, :status) json.board_name notification.card.board.name json.has_attachments notification.card.has_attachments? + json.assignees notification.card.assignees, partial: "users/user", as: :user json.url card_url(notification.card) json.column notification.card.column, partial: "columns/column", as: :column if notification.card.column end From a844353f1e0c7af0f415763f75e5531a19af32c2 Mon Sep 17 00:00:00 2001 From: Fernando Olivares Date: Thu, 22 Jan 2026 03:31:23 -0600 Subject: [PATCH 07/15] Remove unused has_attachments and assignees from notification JSON These fields were added but are not used by the iOS client. Co-Authored-By: Claude Opus 4.5 --- app/views/notifications/_notification.json.jbuilder | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/notifications/_notification.json.jbuilder b/app/views/notifications/_notification.json.jbuilder index 7d269d46d..5673097a7 100644 --- a/app/views/notifications/_notification.json.jbuilder +++ b/app/views/notifications/_notification.json.jbuilder @@ -11,8 +11,6 @@ json.cache! notification do json.card do json.(notification.card, :id, :number, :title, :status) json.board_name notification.card.board.name - json.has_attachments notification.card.has_attachments? - json.assignees notification.card.assignees, partial: "users/user", as: :user json.url card_url(notification.card) json.column notification.card.column, partial: "columns/column", as: :column if notification.card.column end From 80546a6b477b8ada4ed4349cc018b6d8891e9bdd Mon Sep 17 00:00:00 2001 From: Alp Keser Date: Wed, 28 Jan 2026 16:43:39 +0300 Subject: [PATCH 08/15] Include read notifications in the tray API response --- .../notifications/trays_controller.rb | 20 ++++++++++++++++++- .../notifications/trays_controller_test.rb | 13 +++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/controllers/notifications/trays_controller.rb b/app/controllers/notifications/trays_controller.rb index 0adc0e17b..980d1ba1f 100644 --- a/app/controllers/notifications/trays_controller.rb +++ b/app/controllers/notifications/trays_controller.rb @@ -1,9 +1,27 @@ class Notifications::TraysController < ApplicationController + MAX_ENTRIES_LIMIT = 100 + def show - @notifications = Current.user.notifications.unread.preloaded.ordered.limit(100) + @notifications = unread_notifications + if include_unread? + @notifications += read_notifications + end # Invalidate on the whole set instead of the unread set since the max updated at in the unread set # can stay the same when reading old notifications. fresh_when Current.user.notifications end + + private + def unread_notifications + Current.user.notifications.unread.preloaded.ordered.limit(MAX_ENTRIES_LIMIT) + end + + def read_notifications + Current.user.notifications.read.preloaded.ordered.limit(MAX_ENTRIES_LIMIT) + end + + def include_unread? + ActiveModel::Type::Boolean.new.cast(params[:include_unread]) + end end diff --git a/test/controllers/notifications/trays_controller_test.rb b/test/controllers/notifications/trays_controller_test.rb index d0916e305..89dc42c6f 100644 --- a/test/controllers/notifications/trays_controller_test.rb +++ b/test/controllers/notifications/trays_controller_test.rb @@ -18,6 +18,17 @@ class Notifications::TraysControllerTest < ActionDispatch::IntegrationTest get tray_notifications_path(format: :json) assert_response :success - assert_equal expected_ids, @response.parsed_body.map { |notification| notification["id"] } + assert_equal expected_ids, @response.parsed_body.map { |s| s["id"] } + end + + test "show as JSON with include_unread includes read notifications" do + notifications = users(:kevin).notifications + expected_ids = notifications.unread.ordered.limit(100).pluck(:id) + + notifications.read.ordered.limit(100).pluck(:id) + + get tray_notifications_path(format: :json, include_unread: true) + + assert_response :success + assert_equal expected_ids, @response.parsed_body.map { |s| s["id"] } end end From b8793bfec36beb44633bc6a453b77a4198822431 Mon Sep 17 00:00:00 2001 From: Alp Keser Date: Thu, 29 Jan 2026 10:18:20 +0300 Subject: [PATCH 09/15] Add include_unread param to tray API and vary cache --- app/controllers/notifications/trays_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/notifications/trays_controller.rb b/app/controllers/notifications/trays_controller.rb index 980d1ba1f..fac108370 100644 --- a/app/controllers/notifications/trays_controller.rb +++ b/app/controllers/notifications/trays_controller.rb @@ -9,7 +9,7 @@ class Notifications::TraysController < ApplicationController # Invalidate on the whole set instead of the unread set since the max updated at in the unread set # can stay the same when reading old notifications. - fresh_when Current.user.notifications + fresh_when etag: [ Current.user.notifications, include_unread? ] end private From a6eb331710e128f1585b08d64467fdc034f08f13 Mon Sep 17 00:00:00 2001 From: Alp Keser Date: Tue, 3 Feb 2026 11:51:19 +0300 Subject: [PATCH 10/15] Fix query parameter name --- app/controllers/notifications/trays_controller.rb | 8 ++++---- test/controllers/notifications/trays_controller_test.rb | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/notifications/trays_controller.rb b/app/controllers/notifications/trays_controller.rb index fac108370..46dc445cf 100644 --- a/app/controllers/notifications/trays_controller.rb +++ b/app/controllers/notifications/trays_controller.rb @@ -3,13 +3,13 @@ class Notifications::TraysController < ApplicationController def show @notifications = unread_notifications - if include_unread? + if include_read? @notifications += read_notifications end # Invalidate on the whole set instead of the unread set since the max updated at in the unread set # can stay the same when reading old notifications. - fresh_when etag: [ Current.user.notifications, include_unread? ] + fresh_when etag: [ Current.user.notifications, include_read? ] end private @@ -21,7 +21,7 @@ class Notifications::TraysController < ApplicationController Current.user.notifications.read.preloaded.ordered.limit(MAX_ENTRIES_LIMIT) end - def include_unread? - ActiveModel::Type::Boolean.new.cast(params[:include_unread]) + def include_read? + ActiveModel::Type::Boolean.new.cast(params[:include_read]) end end diff --git a/test/controllers/notifications/trays_controller_test.rb b/test/controllers/notifications/trays_controller_test.rb index 89dc42c6f..b6efa200b 100644 --- a/test/controllers/notifications/trays_controller_test.rb +++ b/test/controllers/notifications/trays_controller_test.rb @@ -21,12 +21,12 @@ class Notifications::TraysControllerTest < ActionDispatch::IntegrationTest assert_equal expected_ids, @response.parsed_body.map { |s| s["id"] } end - test "show as JSON with include_unread includes read notifications" do + test "show as JSON with include_read includes read notifications" do notifications = users(:kevin).notifications expected_ids = notifications.unread.ordered.limit(100).pluck(:id) + notifications.read.ordered.limit(100).pluck(:id) - get tray_notifications_path(format: :json, include_unread: true) + get tray_notifications_path(format: :json, include_read: true) assert_response :success assert_equal expected_ids, @response.parsed_body.map { |s| s["id"] } From b755522a7323d465e78e89c8be6fb9cd5b44beda Mon Sep 17 00:00:00 2001 From: Alp Keser Date: Tue, 3 Feb 2026 11:52:02 +0300 Subject: [PATCH 11/15] Add notification.source_type.underscore --- app/views/notifications/_notification.json.jbuilder | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/notifications/_notification.json.jbuilder b/app/views/notifications/_notification.json.jbuilder index 5673097a7..d4dce9fff 100644 --- a/app/views/notifications/_notification.json.jbuilder +++ b/app/views/notifications/_notification.json.jbuilder @@ -3,6 +3,7 @@ json.cache! notification do json.read notification.read? json.read_at notification.read_at&.utc json.created_at notification.created_at.utc + json.source_type notification.source_type.underscore json.partial! "notifications/notification/#{notification.source_type.underscore}/body", notification: notification From abcbb74ac8253069e9ec9d31e8e7a03e6852c13f Mon Sep 17 00:00:00 2001 From: Alp Keser Date: Thu, 12 Feb 2026 15:32:10 +0300 Subject: [PATCH 12/15] Make notification ordering deterministic. --- app/models/notification.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/notification.rb b/app/models/notification.rb index 3d7df5885..70b02de2b 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -9,7 +9,7 @@ class Notification < ApplicationRecord scope :unread, -> { where(read_at: nil) } scope :read, -> { where.not(read_at: nil) } - scope :ordered, -> { order(read_at: :desc, updated_at: :desc) } + scope :ordered, -> { order(read_at: :desc, updated_at: :desc, id: :desc) } scope :preloaded, -> { preload(:card, :creator, :account, source: [ :board, :creator, { eventable: [ :closure, :board, :assignments ] } ]) } before_validation :set_card From c4c46cce442aad255cf4ad806290f5128d4cad8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jirka=20Hut=C3=A1rek?= Date: Fri, 13 Feb 2026 00:45:47 +0100 Subject: [PATCH 13/15] Add postponed and closed flags to Pinned and Notification cards --- app/views/cards/_card.json.jbuilder | 1 + app/views/notifications/_notification.json.jbuilder | 2 ++ test/controllers/cards_controller_test.rb | 1 + test/controllers/notifications_controller_test.rb | 4 ++++ 4 files changed, 8 insertions(+) diff --git a/app/views/cards/_card.json.jbuilder b/app/views/cards/_card.json.jbuilder index f8e727732..bccf51d6e 100644 --- a/app/views/cards/_card.json.jbuilder +++ b/app/views/cards/_card.json.jbuilder @@ -8,6 +8,7 @@ json.cache! card do json.tags card.tags.pluck(:title).sort json.closed card.closed? + json.postponed card.postponed? json.golden card.golden? json.last_active_at card.last_active_at.utc json.created_at card.created_at.utc diff --git a/app/views/notifications/_notification.json.jbuilder b/app/views/notifications/_notification.json.jbuilder index d4dce9fff..ae166f44a 100644 --- a/app/views/notifications/_notification.json.jbuilder +++ b/app/views/notifications/_notification.json.jbuilder @@ -12,6 +12,8 @@ json.cache! notification do json.card do json.(notification.card, :id, :number, :title, :status) json.board_name notification.card.board.name + json.closed notification.card.closed? + json.postponed notification.card.postponed? json.url card_url(notification.card) json.column notification.card.column, partial: "columns/column", as: :column if notification.card.column end diff --git a/test/controllers/cards_controller_test.rb b/test/controllers/cards_controller_test.rb index 8007657d4..b22a8eea6 100644 --- a/test/controllers/cards_controller_test.rb +++ b/test/controllers/cards_controller_test.rb @@ -171,6 +171,7 @@ class CardsControllerTest < ActionDispatch::IntegrationTest assert_equal card.title, @response.parsed_body["title"] assert_equal card.closed?, @response.parsed_body["closed"] + assert_equal card.postponed?, @response.parsed_body["postponed"] assert_equal 2, @response.parsed_body["steps"].size assert_equal card_comments_url(card), @response.parsed_body["comments_url"] assert_equal card_reactions_url(card), @response.parsed_body["reactions_url"] diff --git a/test/controllers/notifications_controller_test.rb b/test/controllers/notifications_controller_test.rb index aa9c283a5..29b454ebd 100644 --- a/test/controllers/notifications_controller_test.rb +++ b/test/controllers/notifications_controller_test.rb @@ -26,5 +26,9 @@ class NotificationsControllerTest < ActionDispatch::IntegrationTest assert_not_nil notification.dig("card", "number") assert_not_nil notification.dig("card", "board_name") assert_not_nil notification.dig("card", "column") + + card = notifications(:logo_assignment_kevin).card + assert_equal card.closed?, notification.dig("card", "closed") + assert_equal card.postponed?, notification.dig("card", "postponed") end end From bcc0f93bb930593b332b84b2596e7ea4d81b0941 Mon Sep 17 00:00:00 2001 From: Alp Keser Date: Mon, 16 Feb 2026 17:58:33 +0300 Subject: [PATCH 14/15] Preload notification card associations used by JSON responses --- app/models/notification.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/notification.rb b/app/models/notification.rb index 70b02de2b..2d868674c 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -10,7 +10,13 @@ class Notification < ApplicationRecord scope :unread, -> { where(read_at: nil) } scope :read, -> { where.not(read_at: nil) } scope :ordered, -> { order(read_at: :desc, updated_at: :desc, id: :desc) } - scope :preloaded, -> { preload(:card, :creator, :account, source: [ :board, :creator, { eventable: [ :closure, :board, :assignments ] } ]) } + scope :preloaded, -> { + preload( + :creator, :account, + card: [ :board, :column, :closure, :not_now ], + source: [ :board, :creator, { eventable: [ :closure, :board, :assignments ] } ] + ) + } before_validation :set_card after_create :bundle From a48c88cb5d6e35bd2000744589dfe085c8dc5385 Mon Sep 17 00:00:00 2001 From: Alp Keser Date: Mon, 16 Feb 2026 18:33:32 +0300 Subject: [PATCH 15/15] Stabilize notifications tray test ordering via fixture updated_at timestamps --- app/models/notification.rb | 2 +- test/fixtures/notifications.yml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/models/notification.rb b/app/models/notification.rb index 2d868674c..417a2f22d 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -9,7 +9,7 @@ class Notification < ApplicationRecord scope :unread, -> { where(read_at: nil) } scope :read, -> { where.not(read_at: nil) } - scope :ordered, -> { order(read_at: :desc, updated_at: :desc, id: :desc) } + scope :ordered, -> { order(read_at: :desc, updated_at: :desc) } scope :preloaded, -> { preload( :creator, :account, diff --git a/test/fixtures/notifications.yml b/test/fixtures/notifications.yml index b7ae167cd..8fa4404d5 100644 --- a/test/fixtures/notifications.yml +++ b/test/fixtures/notifications.yml @@ -5,6 +5,7 @@ logo_assignment_kevin: card: logo_uuid unread_count: 2 created_at: <%= 1.week.ago %> + updated_at: <%= 1.week.ago + 1.second %> creator: david_uuid account: 37s_uuid @@ -15,6 +16,7 @@ layout_commented_kevin: card: layout_uuid unread_count: 1 created_at: <%= 1.week.ago %> + updated_at: <%= 1.week.ago + 2.seconds %> creator: david_uuid account: 37s_uuid @@ -25,5 +27,6 @@ logo_mentioned_david: card: logo_uuid unread_count: 2 created_at: <%= 1.week.ago %> + updated_at: <%= 1.week.ago + 3.seconds %> creator: david_uuid account: 37s_uuid