From c6a243ee591bb13aa49d08f8a6bcb3d2225f9659 Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Sat, 29 Aug 2026 19:28:10 +0800 Subject: [PATCH] Add meeting display fields --- server/im_http.go | 108 +++++++++++++++++++++++++++++++++++++++-- server/im_http_test.go | 19 +++++++- 2 files changed, 122 insertions(+), 5 deletions(-) diff --git a/server/im_http.go b/server/im_http.go index 982c03c..4ecf275 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -1926,8 +1926,9 @@ func serveIMMeetingList(wrt http.ResponseWriter, req *http.Request) { return } items := make([]map[string]any, 0, len(meetings)) + userSummaries := meetingUserSummaries(principal.TenantID, meetings) for i := range meetings { - items = append(items, meetingPayload(&meetings[i], principal.Uid)) + items = append(items, meetingPayloadWithUserSummaries(&meetings[i], principal.Uid, userSummaries)) } writeIMSuccess(wrt, req, map[string]any{ "items": items, @@ -7273,22 +7274,37 @@ func organizationSearchPayloads(items []types.OrganizationSearchItem) []map[stri } func meetingPayload(meeting *types.Meeting, viewer types.Uid) map[string]any { - participants := meetingParticipantPayloads(meeting.Participants) + return meetingPayloadWithUserSummaries(meeting, viewer, nil) +} + +func meetingPayloadWithUserSummaries(meeting *types.Meeting, viewer types.Uid, userSummaries map[types.Uid]map[string]any) map[string]any { + participants := meetingParticipantPayloadsWithSummaries(meeting.Participants, userSummaries) + organizer := meetingUserSummary(meeting.OrganizerID, userSummaries) + startAt := meeting.StartAt.UTC() + endAt := meeting.EndAt.UTC() data := map[string]any{ "meeting_id": meeting.MeetingID, "title": meeting.Title, "organizer_id": meeting.OrganizerID.UserId(), - "start_at": meeting.StartAt.UTC().Format(time.RFC3339Nano), - "end_at": meeting.EndAt.UTC().Format(time.RFC3339Nano), + "organizer": organizer, + "start_at": startAt.Format(time.RFC3339Nano), + "end_at": endAt.Format(time.RFC3339Nano), + "meeting_time": meetingTimeRange(startAt, endAt), + "time_range": meetingTimeRange(startAt, endAt), "location": meeting.Location, "state": meeting.State, "reminder_minutes": meeting.ReminderMinutes, "version": meeting.Version, "participants": participants, + "participant_count": len(participants), + "participant_names": meetingParticipantNames(participants), "permissions": meetingPermissionsPayload(meeting, viewer), "created_at": meeting.CreatedAt.UTC().Format(time.RFC3339Nano), "updated_at": meeting.UpdatedAt.UTC().Format(time.RFC3339Nano), } + if displayName, ok := organizer["display_name"]; ok { + data["organizer_display_name"] = displayName + } if meeting.CanceledAt != nil { data["canceled_at"] = meeting.CanceledAt.UTC().Format(time.RFC3339Nano) } @@ -7302,12 +7318,24 @@ func meetingPayload(meeting *types.Meeting, viewer types.Uid) map[string]any { } func meetingParticipantPayloads(participants []types.MeetingParticipant) []map[string]any { + return meetingParticipantPayloadsWithSummaries(participants, nil) +} + +func meetingParticipantPayloadsWithSummaries(participants []types.MeetingParticipant, userSummaries map[types.Uid]map[string]any) []map[string]any { items := make([]map[string]any, 0, len(participants)) for _, participant := range participants { item := map[string]any{ "user_id": participant.UserID.UserId(), "response": participant.Response, } + if summary := meetingUserSummary(participant.UserID, userSummaries); summary != nil { + if displayName, ok := summary["display_name"]; ok { + item["display_name"] = displayName + } + if avatar, ok := summary["avatar_file_id"]; ok { + item["avatar_file_id"] = avatar + } + } if participant.ResponseAt != nil { item["response_at"] = participant.ResponseAt.UTC().Format(time.RFC3339Nano) } @@ -7316,6 +7344,78 @@ func meetingParticipantPayloads(participants []types.MeetingParticipant) []map[s return items } +func meetingUserSummaries(tenantID types.TenantID, meetings []types.Meeting) map[types.Uid]map[string]any { + if store.Users == nil { + return nil + } + seen := make(map[types.Uid]bool) + uids := make([]types.Uid, 0, len(meetings)*2) + for _, meeting := range meetings { + if !meeting.OrganizerID.IsZero() && !seen[meeting.OrganizerID] { + seen[meeting.OrganizerID] = true + uids = append(uids, meeting.OrganizerID) + } + for _, participant := range meeting.Participants { + if !participant.UserID.IsZero() && !seen[participant.UserID] { + seen[participant.UserID] = true + uids = append(uids, participant.UserID) + } + } + } + if len(uids) == 0 { + return nil + } + users, err := store.Users.GetAll(tenantID, uids...) + if err != nil { + logs.Warn.Printf("im http: failed to load meeting user summaries: %v", err) + return nil + } + summaries := make(map[types.Uid]map[string]any, len(users)) + for i := range users { + uid := users[i].Uid() + if uid.IsZero() { + uid = types.ParseUid(users[i].Id) + } + if !uid.IsZero() { + summaries[uid] = userSummaryPayload(&users[i], "") + } + } + return summaries +} + +func meetingUserSummary(uid types.Uid, summaries map[types.Uid]map[string]any) map[string]any { + if uid.IsZero() { + return nil + } + if summary, ok := summaries[uid]; ok && summary != nil { + return summary + } + return map[string]any{ + "user_id": uid.UserId(), + "display_name": uid.UserId(), + } +} + +func meetingParticipantNames(participants []map[string]any) []string { + names := make([]string, 0, len(participants)) + for _, participant := range participants { + if name := strings.TrimSpace(fmt.Sprint(participant["display_name"])); name != "" { + names = append(names, name) + } + } + return names +} + +func meetingTimeRange(startAt, endAt time.Time) string { + if startAt.IsZero() || endAt.IsZero() { + return "" + } + if startAt.Format("2006-01-02") == endAt.Format("2006-01-02") { + return startAt.Format("2006-01-02 15:04") + "-" + endAt.Format("15:04") + } + return startAt.Format("2006-01-02 15:04") + "-" + endAt.Format("2006-01-02 15:04") +} + func meetingParticipantUIDs(meeting *types.Meeting) []types.Uid { if meeting == nil { return nil diff --git a/server/im_http_test.go b/server/im_http_test.go index e2e1662..ae339bb 100644 --- a/server/im_http_test.go +++ b/server/im_http_test.go @@ -2374,6 +2374,7 @@ func TestServeIMMeetingsList(t *testing.T) { viewer := types.Uid(42) expectIMBearer(t, ctrl, "token-1", 7, viewer) oldMeetings := store.Meetings + oldUsers := store.Users now := time.Now().UTC().Round(time.Millisecond) meetings := &testMeetingsStore{list: []types.Meeting{{ MeetingID: "mtg_test", @@ -2391,7 +2392,15 @@ func TestServeIMMeetingsList(t *testing.T) { }, }}} store.Meetings = meetings - defer func() { store.Meetings = oldMeetings }() + users := mock_store.NewMockUsersPersistenceInterface(ctrl) + organizer := types.User{Public: map[string]any{"fn": "mzb"}} + organizer.SetUid(viewer) + users.EXPECT().GetAll(types.TenantID(7), viewer).Return([]types.User{organizer}, nil) + store.Users = users + defer func() { + store.Meetings = oldMeetings + store.Users = oldUsers + }() req := httptest.NewRequest(http.MethodGet, "/v1/im/meetings?limit=20&cursor=c1", nil) req.Header.Set("Authorization", "Bearer token-1") @@ -2409,6 +2418,14 @@ func TestServeIMMeetingsList(t *testing.T) { if items[0].(map[string]any)["meeting_id"] != "mtg_test" || data["has_more"] != true { t.Fatalf("unexpected meeting payload: %#v", data) } + item := items[0].(map[string]any) + if item["organizer_display_name"] != "mzb" || item["participant_count"].(float64) != 1 { + t.Fatalf("missing meeting display fields: %#v", item) + } + participants := item["participants"].([]any) + if participants[0].(map[string]any)["display_name"] != "mzb" { + t.Fatalf("missing participant display name: %#v", participants) + } } func TestServeIMMeetingMyResponse(t *testing.T) {