0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-11 02:59:07 +08:00
discourse/plugins/discourse-calendar/spec/serializers/topic_view_serializer_spec.rb
Kris dd2c5ba0f8
UX: improve livestream chat messaging (#41750)
This does a handful of things to improve the livestream chat experience:

1. RSVP prompt replaces the default join button. 

A new chat-channel-preview-card-content plugin outlet in core chat lets
the calendar plugin's livestream-rsvp connector fill the redesigned
preview card's content slots. For users who can RSVP, it swaps the
default join button for a going button for the event.
2. Pinned reference message linking back to the event topic. When a
livestream channel is created, a system message is posted and pinned,
and its id is stored in a new `reference_message_id` column. It's hidden
via CSS when the chat is embedded in that same topic because it would be
redundant.
3. Access control for private events. `chat_channel_id` is only
serialized to users who can access the event's chat (invitees/invited
groups, or admins)
4. If the channel is already open and expanded in the chat drawer, the
embedded chat (and the livestream page layout) are suppressed.
5. Livestream channels are created with the spiral calendar emoji 🗓️ as
their icon.

I also converted core chat's preview card from viewport media queries to
container queries so its layout responds to the width of the chat column
it's rendered in rather than the viewport.

With livestream sidebar
<img width="2962" height="1726" alt="image"
src="https://github.com/user-attachments/assets/71dc3d3f-35e5-4a0d-9103-92a03d4bb08e"
/>

Isolated chat when you're not in the livestream topic 
<img width="2560" height="1726" alt="image"
src="https://github.com/user-attachments/assets/51cef0b8-85b4-4be8-a4f3-bf669f4be4fb"
/>

---------

Co-authored-by: Martin Brennan <martin@discourse.org>
2026-07-21 11:07:31 +10:00

230 lines
6.6 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe TopicViewSerializer do
subject(:serializer) do
described_class.new(TopicView.new(topic), scope: Guardian.new, root: false)
end
let(:topic) { Fabricate(:topic) }
let(:first_post) { Fabricate(:post, topic:) }
let(:parsed_json) { JSON.parse(serializer.to_json) }
before do
freeze_time(Time.utc(2020, 4, 24, 14, 10))
Jobs.run_immediately!
SiteSetting.calendar_enabled = true
SiteSetting.discourse_post_event_enabled = true
end
context "without timezone" do
before do
DiscoursePostEvent::Event.create!(
id: first_post.id,
original_starts_at: 1.hour.from_now,
original_ends_at: 2.hours.from_now,
)
end
describe "#event_starts_at" do
it "returns the start time of the event in proper format" do
expect(parsed_json["event_starts_at"]).to eq("2020-04-24T15:10:00.000Z")
end
end
describe "#event_ends_at" do
it "returns the end time of the event in proper format" do
expect(parsed_json["event_ends_at"]).to eq("2020-04-24T16:10:00.000Z")
end
end
describe "#event_timezone" do
it "is not included when event has no timezone" do
expect(parsed_json).not_to have_key("event_timezone")
end
end
describe "#event_show_local_time" do
it "returns false when show_local_time is not set" do
expect(parsed_json["event_show_local_time"]).to eq(false)
end
end
end
context "with timezone and show_local_time true" do
before do
DiscoursePostEvent::Event.create!(
id: first_post.id,
original_starts_at: 1.hour.from_now,
original_ends_at: 2.hours.from_now,
timezone: "Australia/Sydney",
show_local_time: true,
)
end
describe "#event_timezone" do
it "returns the timezone of the event" do
expect(parsed_json["event_timezone"]).to eq("Australia/Sydney")
end
end
describe "#event_show_local_time" do
it "returns true when show_local_time is set" do
expect(parsed_json["event_show_local_time"]).to eq(true)
end
end
end
context "with timezone and show_local_time false" do
before do
DiscoursePostEvent::Event.create!(
id: first_post.id,
original_starts_at: 1.hour.from_now,
original_ends_at: 2.hours.from_now,
timezone: "Australia/Sydney",
show_local_time: false,
)
end
describe "#event_timezone" do
it "returns the timezone of the event" do
expect(parsed_json["event_timezone"]).to eq("Australia/Sydney")
end
end
describe "#event_show_local_time" do
it "returns false when show_local_time is explicitly false" do
expect(parsed_json["event_show_local_time"]).to eq(false)
end
end
end
context "with all-day event" do
before do
SiteSetting.display_post_event_date_on_topic_title = true
DiscoursePostEvent::Event.create!(
id: first_post.id,
original_starts_at: Time.utc(2020, 4, 25),
original_ends_at: Time.utc(2020, 4, 27),
all_day: true,
)
end
describe "#event_all_day" do
it "returns true" do
expect(parsed_json["event_all_day"]).to eq(true)
end
end
end
context "without all-day event" do
before do
DiscoursePostEvent::Event.create!(
id: first_post.id,
original_starts_at: 1.hour.from_now,
original_ends_at: 2.hours.from_now,
)
end
describe "#event_all_day" do
it "is not included" do
expect(parsed_json).not_to have_key("event_all_day")
end
end
end
describe "#chat_channel_id" do
fab!(:category)
fab!(:viewer, :user)
let(:topic) { Fabricate(:topic, category:) }
before do
SiteSetting.chat_enabled = true
# Don't do onebox-warming job so it doesn't make a real request
Jobs.run_later!
first_post
end
def create_event(livestream:, status: :public, raw_invitees: nil)
DiscoursePostEvent::Event.create!(
id: first_post.id,
original_starts_at: 1.hour.from_now,
original_ends_at: 2.hours.from_now,
location: "https://example.com/live",
status: DiscoursePostEvent::Event.statuses[status],
raw_invitees:,
livestream:,
)
end
def parsed_json_for(user)
JSON.parse(
described_class.new(TopicView.new(topic), scope: Guardian.new(user), root: false).to_json,
)
end
it "is included while the event is a livestream" do
create_event(livestream: true)
expect(topic.topic_chat_channel).to be_present
expect(parsed_json_for(viewer)["chat_channel_id"]).to eq(
topic.topic_chat_channel.chat_channel_id,
)
end
it "is not included once livestream is disabled, even if the channel row remains" do
event = create_event(livestream: true)
event.update!(livestream: false)
expect(topic.reload.topic_chat_channel).to be_present
expect(parsed_json_for(viewer)).not_to have_key("chat_channel_id")
end
it "is not included for users who cannot join the event" do
create_event(livestream: true, status: :private, raw_invitees: ["some_group"])
expect(parsed_json_for(viewer)).not_to have_key("chat_channel_id")
end
it "is not included for anonymous users on private events" do
create_event(livestream: true, status: :private, raw_invitees: ["some_group"])
expect(parsed_json_for(nil)).not_to have_key("chat_channel_id")
end
it "is included for anonymous users on public events" do
create_event(livestream: true)
expect(parsed_json_for(nil)["chat_channel_id"]).to eq(
topic.topic_chat_channel.chat_channel_id,
)
end
it "is still included once the event is expired" do
create_event(livestream: true)
freeze_time(3.hours.from_now)
expect(parsed_json_for(viewer)["chat_channel_id"]).to eq(
topic.topic_chat_channel.chat_channel_id,
)
end
it "is included for users in a group that can join the event" do
group = Fabricate(:group)
group.add(viewer)
create_event(livestream: true, status: :private, raw_invitees: [group.name])
expect(parsed_json_for(viewer)["chat_channel_id"]).to eq(
topic.topic_chat_channel.chat_channel_id,
)
end
it "is included for admins even when they cannot join the event" do
create_event(livestream: true, status: :private, raw_invitees: ["some_group"])
expect(parsed_json_for(Fabricate(:admin))["chat_channel_id"]).to eq(
topic.topic_chat_channel.chat_channel_id,
)
end
end
end