diff --git a/app/assets/images/notification-bell-access-only.svg b/app/assets/images/notification-bell-access-only.svg
deleted file mode 100644
index b8fa159e8..000000000
--- a/app/assets/images/notification-bell-access-only.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/app/assets/images/notification-bell-everything.svg b/app/assets/images/notification-bell-everything.svg
deleted file mode 100644
index 52a5611fb..000000000
--- a/app/assets/images/notification-bell-everything.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/app/assets/images/notification-bell-watching.svg b/app/assets/images/notification-bell-watching.svg
deleted file mode 100644
index 05827531d..000000000
--- a/app/assets/images/notification-bell-watching.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css
index ec90b8e58..1f1c7dd68 100644
--- a/app/assets/stylesheets/card-columns.css
+++ b/app/assets/stylesheets/card-columns.css
@@ -92,29 +92,25 @@
display: none;
}
- .card__background {
- .card:has(& img:not([src=""])) {
- .card__content,
- .card__meta,
- .workflow-stage:not(.workflow-stage--current) {
- opacity: 0;
- transition: opacity 0.2s ease-in-out;
+ .card:has(.card__background img:not([src=""])) {
+ .card__content,
+ .card__meta,
+ .workflow-stage:not(.workflow-stage--current) {
+ opacity: 0;
+ transition: opacity 0.2s ease-in-out;
- @media (any-hover: hover) {
- .card:hover & {
+ @media (any-hover: hover) {
+ .card:hover & {
opacity: 1;
}
- }
}
}
+ }
- @media (any-hover: hover) {
- .card:hover & {
- img:not([src=""]) {
- filter: blur(3px) brightness(1.2);
- opacity: 0.2;
- }
- }
+ @media (any-hover: hover) {
+ .card:has(.card__background img:not([src=""])):hover .card__background img:not([src=""]) {
+ filter: blur(3px) brightness(1.2);
+ opacity: 0.2;
}
}
@@ -262,16 +258,27 @@
}
.card--new {
- --card-padding-block: var(--block-space-double);
+ --border-color: var(--color-selected-dark);
+ --border-size: 1px;
+ --card-padding-block: var(--block-space);
border: 1px solid var(--color-selected-dark);
- font-size: 0.8rem !important;
- rotate: 0 !important;
text-align: center;
.btn {
- border: 0;
- color: var(--color-considering);
+ &.btn--link {
+ font-size: 1.2em;
+ }
+
+ &:not(.btn--link) {
+ border: 0;
+ color: var(--color-link);
+ }
+ }
+
+ footer {
+ font-size: var(--text-x-small);
+ margin-block-end: calc(var(--block-space) / -2);
}
}
diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css
index 7a427a6d2..f0ae80af0 100644
--- a/app/assets/stylesheets/cards.css
+++ b/app/assets/stylesheets/cards.css
@@ -312,11 +312,11 @@
position: absolute;
rotate: 5deg;
transform-origin: top right;
+ }
- .card:has(&),
- .card-perma:has(&) {
- --card-color: var(--color-card-complete) !important;
- }
+ .card:has(.card__closed),
+ .card-perma:has(.card__closed) {
+ --card-color: var(--color-card-complete) !important;
}
.card__closed-title {
diff --git a/app/assets/stylesheets/filters.css b/app/assets/stylesheets/filters.css
index 5b621bbf9..298c2744b 100644
--- a/app/assets/stylesheets/filters.css
+++ b/app/assets/stylesheets/filters.css
@@ -3,11 +3,14 @@
position: relative;
view-transition-name: "filters";
z-index: var(--z-popup);
+ }
- #header:has(&) {
- position: relative;
- z-index: var(--z-popup);
- }
+ #header:has(.filters) {
+ position: relative;
+ z-index: var(--z-popup);
+ }
+
+ .filters {
.btn {
--btn-border-color: var(--color-ink-medium);
@@ -114,5 +117,17 @@
display: none;
}
}
+
+ .filters:not(.filters--expanded) {
+ .quick-filter:not([data-filter-show=true]) {
+ display: none;
+ }
+ }
+
+ .filters.filters--expanded {
+ .quick-filter {
+ display: block;
+ }
+ }
}
diff --git a/app/assets/stylesheets/header.css b/app/assets/stylesheets/header.css
index 2d5bc40a9..d8b5a2a43 100644
--- a/app/assets/stylesheets/header.css
+++ b/app/assets/stylesheets/header.css
@@ -14,7 +14,6 @@
"actions-start title actions-end";
max-inline-size: 100dvw;
padding: calc(var(--block-space-half) + env(safe-area-inset-top)) var(--main-padding);
- view-transition-name: header;
/* Change the grid size depending on how many buttons are present */
&:has(.header__actions > *:nth-child(1)) { --header-button-count: 1; }
diff --git a/app/assets/stylesheets/icons.css b/app/assets/stylesheets/icons.css
index b476cc73b..6eab28b1b 100644
--- a/app/assets/stylesheets/icons.css
+++ b/app/assets/stylesheets/icons.css
@@ -61,9 +61,8 @@
.icon--menu-dots-vertical { --svg: url("menu-dots-vertical.svg "); }
.icon--minus { --svg: url("minus.svg "); }
.icon--move { --svg: url("move.svg "); }
- .icon--notification-bell-access-only { --svg: url("notification-bell-access-only.svg "); }
- .icon--notification-bell-everything { --svg: url("notification-bell-everything.svg "); }
- .icon--notification-bell-watching { --svg: url("notification-bell-watching.svg "); }
+ .icon--notification-bell-access-only { --svg: url("bell.svg "); }
+ .icon--notification-bell-watching { --svg: url("bell-off.svg "); }
.icon--password { --svg: url("password.svg "); }
.icon--pencil { --svg: url("pencil.svg "); }
.icon--person { --svg: url("person.svg "); }
diff --git a/app/assets/stylesheets/inputs.css b/app/assets/stylesheets/inputs.css
index 1a9afd70a..e0ca57536 100644
--- a/app/assets/stylesheets/inputs.css
+++ b/app/assets/stylesheets/inputs.css
@@ -60,13 +60,11 @@
cursor: pointer;
opacity: 0;
}
+ }
- &:focus,
- &:focus-visible {
- .input--file:has(&) {
- outline: 0.15rem solid var(--color-selected-dark);
- }
- }
+ &:has(input[type="file"]:focus),
+ &:has(input[type="file"]:focus-visible) {
+ outline: 0.15rem solid var(--color-selected-dark);
}
&:is(.avatar) {
diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css
index b5ca20116..f1e0fc6d4 100644
--- a/app/assets/stylesheets/rich-text-content.css
+++ b/app/assets/stylesheets/rich-text-content.css
@@ -50,11 +50,12 @@
margin-inline: auto;
max-block-size: 32rem;
object-fit: contain;
+ }
- /* Links should hug media contained within */
- a:has(&) {
- display: inline-block;
- }
+ /* Links should hug media contained within */
+ a:has(img),
+ a:has(video) {
+ display: inline-block;
}
/* Avoid extra space due to empty paragraphs */
diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb
index 8c7af5074..1bdd201c0 100644
--- a/app/controllers/cards_controller.rb
+++ b/app/controllers/cards_controller.rb
@@ -8,7 +8,7 @@ class CardsController < ApplicationController
enable_collection_filtering only: :index
- PAGE_SIZE = 50
+ PAGE_SIZE = 25
def index
@considering = page_and_filter_for @filter.with(engagement_status: "considering"), per_page: PAGE_SIZE
@@ -16,7 +16,7 @@ class CardsController < ApplicationController
@doing = page_and_filter_for @filter.with(engagement_status: "doing"), per_page: PAGE_SIZE
@closed = page_and_filter_for_closed_cards
- @cache_key = [ @considering, @on_deck, @doing, @closed ].collect { it.page.records }.including([ Workflow.all ])
+ @cache_key = [ @considering, @on_deck, @doing, @closed ].collect { it.page.records }.including([ Workflow.all, @user_filtering ])
fresh_when etag: @cache_key
end
diff --git a/app/controllers/concerns/filter_scoped.rb b/app/controllers/concerns/filter_scoped.rb
index 6f2dd87de..f191939b9 100644
--- a/app/controllers/concerns/filter_scoped.rb
+++ b/app/controllers/concerns/filter_scoped.rb
@@ -24,7 +24,11 @@ module FilterScoped
end
def set_user_filtering
- @user_filtering = User::Filtering.new(Current.user, @filter, expanded: params[:expand_all])
+ @user_filtering = User::Filtering.new(Current.user, @filter, expanded: expanded_param)
+ end
+
+ def expanded_param
+ ActiveRecord::Type::Boolean.new.cast(params[:expand_all])
end
def enable_collection_filtering
diff --git a/app/controllers/my/menus_controller.rb b/app/controllers/my/menus_controller.rb
new file mode 100644
index 000000000..a81705df3
--- /dev/null
+++ b/app/controllers/my/menus_controller.rb
@@ -0,0 +1,7 @@
+class My::MenusController < ApplicationController
+ include FilterScoped
+
+ def show
+ fresh_when @user_filtering
+ end
+end
diff --git a/app/helpers/accesses_helper.rb b/app/helpers/accesses_helper.rb
index 5eb39466c..9a59643d3 100644
--- a/app/helpers/accesses_helper.rb
+++ b/app/helpers/accesses_helper.rb
@@ -15,34 +15,60 @@ module AccessesHelper
cached: ->(user) { [ user, selected ] }
end
- def access_involvement_advance_button(collection, user)
+ def access_involvement_advance_button(collection, user, show_watchers: true)
access = collection.access_for(user)
turbo_frame_tag dom_id(collection, :involvement_button) do
- button_to collection_involvement_path(collection), method: :put,
- aria: { labelledby: dom_id(collection, :involvement_label) },
- class: [ "btn tooltip", { "btn--reversed": access.involvement == "watching" || access.involvement == "everything" } ],
- params: { involvement: next_involvement(access.involvement) } do
- icon_tag("notification-bell-#{access.involvement.dasherize}") +
- tag.span(involvement_access_label(collection, access.involvement), class: "for-screen-reader", id: dom_id(collection, :involvement_label))
- end
+ concat collection_watchers_list(collection) if show_watchers
+ concat involvement_button(collection, access, show_watchers)
+ end
+ end
+
+ def collection_watchers_list(collection)
+ tag.div(class: "flex flex-wrap gap-half justify-center") do
+ safe_join(
+ collection.users
+ .without(User.system)
+ .where(accesses: { involvement: :watching })
+ .distinct
+ .map { |watcher| avatar_tag(watcher) }
+ )
+ end
+ end
+
+ def involvement_button(collection, access, show_watchers)
+ involvement_label_id = dom_id(collection, :involvement_label)
+
+ button_to(
+ collection_involvement_path(collection),
+ method: :put,
+ aria: { labelledby: involvement_label_id },
+ class: "btn",
+ params: { show_watchers: show_watchers, involvement: next_involvement(access.involvement) }
+ ) do
+ safe_join([
+ icon_tag("notification-bell-#{access.involvement.dasherize}"),
+ tag.span(
+ involvement_access_label(collection, access.involvement),
+ class: "txt-nowrap txt-uppercase",
+ id: involvement_label_id
+ )
+ ])
end
end
private
def next_involvement(involvement)
- order = %w[ everything watching access_only ]
+ order = %w[ watching access_only ]
order[(order.index(involvement.to_s) + 1) % order.size]
end
def involvement_access_label(collection, involvement)
case involvement
when "access_only"
- "Notifications are off for #{collection.name}"
- when "everything"
- "Notifying me about everything in #{collection.name}"
+ "Watch this"
when "watching"
- "Notifying me only about @mentions and new items in #{collection.name}"
+ "Stop Watching"
end
end
end
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
index 99ece604c..53fa055b5 100644
--- a/app/helpers/time_helper.rb
+++ b/app/helpers/time_helper.rb
@@ -1,6 +1,6 @@
module TimeHelper
def local_datetime_tag(datetime, style: :time, **attributes)
# Render empty space to ensure it takes height until the local time is loaded via JS
- tag.time " ".html_safe, **attributes, datetime: datetime.iso8601, data: { local_time_target: style, action: "turbo:morph-element->local-time#refreshTarget" }
+ tag.time " ".html_safe, **attributes, datetime: datetime.to_i, data: { local_time_target: style, action: "turbo:morph-element->local-time#refreshTarget" }
end
end
diff --git a/app/javascript/controllers/form_controller.js b/app/javascript/controllers/form_controller.js
index 64fc6fad6..eaf246fea 100644
--- a/app/javascript/controllers/form_controller.js
+++ b/app/javascript/controllers/form_controller.js
@@ -1,12 +1,30 @@
import { Controller } from "@hotwired/stimulus"
+import { debounce } from "helpers/timing_helpers";
export default class extends Controller {
static targets = [ "cancel" ]
+ static values = {
+ debounceTimeout: { type: Number, default: 300 }
+ }
+
+ initialize() {
+ this.debouncedSubmit = debounce(this.debouncedSubmit.bind(this), this.debounceTimeoutValue)
+ }
+
submit() {
this.element.requestSubmit()
}
+ debouncedSubmit(event) {
+ this.submit(event)
+ }
+
+ submitToTopTarget(event) {
+ this.element.setAttribute("data-turbo-frame", "_top")
+ this.submit()
+ }
+
cancel() {
this.cancelTarget?.click()
}
diff --git a/app/javascript/controllers/local_time_controller.js b/app/javascript/controllers/local_time_controller.js
index 69693a107..d38420332 100644
--- a/app/javascript/controllers/local_time_controller.js
+++ b/app/javascript/controllers/local_time_controller.js
@@ -1,5 +1,5 @@
import { Controller } from "@hotwired/stimulus"
-import { differenceInDays } from "helpers/date_helpers"
+import { differenceInDays, secondsToDate } from "helpers/date_helpers"
const DEFAULT_LOCALE = "en-US"
@@ -89,7 +89,7 @@ export default class extends Controller {
}
#formatTime(formatter, target) {
- const dt = new Date(target.getAttribute("datetime"))
+ const dt = secondsToDate(parseInt(target.getAttribute("datetime")))
target.innerHTML = formatter.format(dt)
target.title = this.datetimeFormatter.format(dt)
}
diff --git a/app/javascript/controllers/toggle_enable_controller.js b/app/javascript/controllers/toggle_enable_controller.js
new file mode 100644
index 000000000..b65ea57d4
--- /dev/null
+++ b/app/javascript/controllers/toggle_enable_controller.js
@@ -0,0 +1,11 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static targets = [ "element" ]
+
+ toggle() {
+ this.elementTargets.forEach((element) => {
+ element.toggleAttribute("disabled")
+ })
+ }
+}
diff --git a/app/javascript/helpers/date_helpers.js b/app/javascript/helpers/date_helpers.js
index 529b2968e..b8d9855f4 100644
--- a/app/javascript/helpers/date_helpers.js
+++ b/app/javascript/helpers/date_helpers.js
@@ -9,3 +9,7 @@ export function signedDifferenceInDays(fromDate, toDate) {
export function beginningOfDay(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
}
+
+export function secondsToDate(seconds) {
+ return new Date(seconds * 1000)
+}
diff --git a/app/models/access.rb b/app/models/access.rb
index 88303bd15..8c1b67a51 100644
--- a/app/models/access.rb
+++ b/app/models/access.rb
@@ -1,8 +1,8 @@
class Access < ApplicationRecord
belongs_to :collection
- belongs_to :user
+ belongs_to :user, touch: true
- enum :involvement, %i[ access_only watching everything ].index_by(&:itself)
+ enum :involvement, %i[ access_only watching ].index_by(&:itself)
scope :ordered_by_recently_accessed, -> { order(accessed_at: :desc) }
diff --git a/app/models/card/watchable.rb b/app/models/card/watchable.rb
index f511ddc7e..883975398 100644
--- a/app/models/card/watchable.rb
+++ b/app/models/card/watchable.rb
@@ -21,7 +21,7 @@ module Card::Watchable
end
def watchers_and_subscribers(include_only_watching: false)
- involvements = include_only_watching ? [ :watching, :everything ] : :everything
+ involvements = include_only_watching ? [ :watching ] : []
subscribers = collection.users.where(accesses: { involvement: involvements })
User.where(id: subscribers.pluck(:id) +
diff --git a/app/models/event/summarizer.rb b/app/models/event/summarizer.rb
index 5d5470d55..c46ff2c08 100644
--- a/app/models/event/summarizer.rb
+++ b/app/models/event/summarizer.rb
@@ -4,7 +4,7 @@ class Event::Summarizer
attr_reader :events
- LLM_MODEL = "chatgpt-4o-latest"
+ LLM_MODEL = "gpt-5-chat-latest"
PROMPT = <<~PROMPT
Help me make sense of the week’s activity in a news style format with bold headlines and short summaries.
diff --git a/app/models/filter.rb b/app/models/filter.rb
index 4748e54a4..6bef679e5 100644
--- a/app/models/filter.rb
+++ b/app/models/filter.rb
@@ -34,7 +34,7 @@ class Filter < ApplicationRecord
result = result.closed_at_window(closure_window) if closure_window
result = result.closed_by(closers) if closers.present?
result = terms.reduce(result) do |result, term|
- result.similar_to(term)
+ result.mentioning(term)
end
result
diff --git a/app/models/filter/fields.rb b/app/models/filter/fields.rb
index a261a23f4..af0966d2a 100644
--- a/app/models/filter/fields.rb
+++ b/app/models/filter/fields.rb
@@ -47,6 +47,10 @@ module Filter::Fields
def terms
Array(super)
end
+
+ def terms=(value)
+ super(Array(value).filter(&:present?))
+ end
end
def with(**fields)
diff --git a/app/models/notifier/card_event_notifier.rb b/app/models/notifier/card_event_notifier.rb
index f36a1f12c..15768ebc3 100644
--- a/app/models/notifier/card_event_notifier.rb
+++ b/app/models/notifier/card_event_notifier.rb
@@ -6,7 +6,7 @@ class Notifier::CardEventNotifier < Notifier
def recipients
case source.action
when "card_assigned"
- source.assignees.excluding(creator, *source.collection.access_only_users)
+ source.assignees.excluding(creator)
when "card_published"
watchers_and_subscribers(include_only_watching: true).without(creator, *card.mentionees)
when "comment_created"
diff --git a/app/models/user/filtering.rb b/app/models/user/filtering.rb
index 1781e356d..f8186ed0e 100644
--- a/app/models/user/filtering.rb
+++ b/app/models/user/filtering.rb
@@ -47,28 +47,28 @@ class User::Filtering
end
def show_indexed_by?
- expanded? || !filter.indexed_by.all?
+ !filter.indexed_by.all?
end
def show_sorted_by?
- expanded? || !filter.sorted_by.latest?
+ !filter.sorted_by.latest?
end
def show_tags?
return unless Tag.any?
- expanded? || filter.tags.any?
+ filter.tags.any?
end
def show_assignees?
- expanded? || filter.assignees.any?
+ filter.assignees.any?
end
def show_creators?
- expanded? || filter.creators.any?
+ filter.creators.any?
end
def show_closers?
- expanded? || filter.closers.any?
+ filter.closers.any?
end
def enable_collection_filtering(&block)
diff --git a/app/views/cards/index.html.erb b/app/views/cards/index.html.erb
index ad3470094..59128e22e 100644
--- a/app/views/cards/index.html.erb
+++ b/app/views/cards/index.html.erb
@@ -1,4 +1,5 @@
<% @page_title = @user_filtering.selected_collections_label %>
+<% turbo_exempts_page_from_cache %>
<%= render "filters/broadcasts", filter: @filter %>
@@ -11,11 +12,7 @@
<% content_for :header do %>
<%= render "filters/menu", user_filtering: @user_filtering %>
-
+
- <% end %>
- <% end %>
-<% end %>
diff --git a/app/views/cards/index/_columns.html.erb b/app/views/cards/index/_columns.html.erb
index a008d1513..9104145fc 100644
--- a/app/views/cards/index/_columns.html.erb
+++ b/app/views/cards/index/_columns.html.erb
@@ -1,11 +1,13 @@
<% cache @cache_key do %>
-
- <% unless user_filtering.only_closed? %>
- <%= render "cards/index/engagement/on_deck", user_filtering: user_filtering, **on_deck.to_h %>
- <%= render "cards/index/engagement/considering", user_filtering: user_filtering, **considering.to_h %>
- <%= render "cards/index/engagement/doing", user_filtering: user_filtering, **doing.to_h %>
- <% end %>
-
+ <%= turbo_frame_tag :card_columns do %>
+
+ <% unless user_filtering.only_closed? %>
+ <%= render "cards/index/engagement/on_deck", user_filtering: user_filtering, **on_deck.to_h %>
+ <%= render "cards/index/engagement/considering", user_filtering: user_filtering, **considering.to_h %>
+ <%= render "cards/index/engagement/doing", user_filtering: user_filtering, **doing.to_h %>
+ <% end %>
+
- <%= render "cards/index/engagement/closed", user_filtering: user_filtering, **closed.to_h %>
+ <%= render "cards/index/engagement/closed", user_filtering: user_filtering, **closed.to_h %>
+ <% end %>
<% end %>
diff --git a/app/views/cards/show/_collections.html.erb b/app/views/cards/show/_collections.html.erb
deleted file mode 100644
index c770471cd..000000000
--- a/app/views/cards/show/_collections.html.erb
+++ /dev/null
@@ -1,51 +0,0 @@
-
-<%= form_with url: cards_path, method: :get, class: "display-contents",
- data: { controller: "form" } do |form| %>
- <% if Current.user.collections.one? %>
- <%= link_to cards_path, class: "popup__item", data: { filter_target: "item", navigable_list_target: "item" } do %>
- <%= tag.div class: "popup__btn btn" do %>
- <%= Current.user.collections.first.name %>
- GO TO ›
- <% end %>
- <% end %>
- <% elsif Current.user.collections.many? %>
-
- <%= link_to cards_path, class: "popup__item", data: { filter_target: "item", navigable_list_target: "item" } do %>
-
- <%= tag.div class: "popup__btn btn" do %>
- All collections
- GO TO ›
- <% end %>
- <% end %>
-
-
- <% Current.user.collections.ordered_by_recently_accessed.each do |collection| %>
-
- <% end %>
- <% end %>
-<% end %>
diff --git a/app/views/collections/involvements/update.html.erb b/app/views/collections/involvements/update.html.erb
index b72570962..28c5cfe0e 100644
--- a/app/views/collections/involvements/update.html.erb
+++ b/app/views/collections/involvements/update.html.erb
@@ -1 +1 @@
-<%= access_involvement_advance_button(@collection, Current.user) %>
+<%= access_involvement_advance_button(@collection, Current.user, show_watchers: params[:show_watchers] == 'true') %>
diff --git a/app/views/filters/_menu.html.erb b/app/views/filters/_menu.html.erb
index ec8920940..87ee55f10 100644
--- a/app/views/filters/_menu.html.erb
+++ b/app/views/filters/_menu.html.erb
@@ -1,43 +1,13 @@
-<% cache user_filtering do %>
-
diff --git a/app/views/filters/_settings.html.erb b/app/views/filters/_settings.html.erb
index a8e875c3e..6b2278817 100644
--- a/app/views/filters/_settings.html.erb
+++ b/app/views/filters/_settings.html.erb
@@ -1,17 +1,13 @@
<% cache user_filtering do %>
-