diff --git a/packages/api/src/elastic/pages.ts b/packages/api/src/elastic/pages.ts index 315515a0b..b93406192 100644 --- a/packages/api/src/elastic/pages.ts +++ b/packages/api/src/elastic/pages.ts @@ -187,14 +187,23 @@ const appendIdsFilter = (body: SearchBody, ids: string[]): void => { } const appendRecommendedBy = (body: SearchBody, recommendedBy: string): void => { + const query = + recommendedBy === '*' + ? { + exists: { + field: 'recommendations', + }, + } + : { + term: { + 'recommendations.name': recommendedBy, + }, + } + body.query.bool.must.push({ nested: { path: 'recommendations', - query: { - term: { - 'recommendations.name': recommendedBy, - }, - }, + query, }, }) } diff --git a/packages/api/src/elastic/recommendation.ts b/packages/api/src/elastic/recommendation.ts index 5c68e206a..daba6a169 100644 --- a/packages/api/src/elastic/recommendation.ts +++ b/packages/api/src/elastic/recommendation.ts @@ -1,4 +1,9 @@ -import { Page, PageContext, Recommendation } from './types' +import { + ArticleSavingRequestStatus, + Page, + PageContext, + Recommendation, +} from './types' import { createPage, getPageByParam, updatePage } from './pages' export const addRecommendation = async ( @@ -66,7 +71,14 @@ export const addRecommendation = async ( sharedAt: new Date(), highlights, readAt: undefined, - labels: [], + labels: undefined, + subscription: undefined, + unsubHttpUrl: undefined, + unsubMailTo: undefined, + _id: undefined, + archivedAt: undefined, + state: ArticleSavingRequestStatus.Succeeded, + taskName: undefined, } return createPage(newPage, ctx) diff --git a/packages/api/src/elastic/types.ts b/packages/api/src/elastic/types.ts index 070cf39ec..b27cef16d 100644 --- a/packages/api/src/elastic/types.ts +++ b/packages/api/src/elastic/types.ts @@ -55,7 +55,10 @@ export interface SearchBody { nested: { path: 'recommendations' query: { - term: { + exists?: { + field: string + } + term?: { 'recommendations.name': string } } diff --git a/packages/api/src/entity/groups/group.ts b/packages/api/src/entity/groups/group.ts index 4aa22e1ee..69c35420c 100644 --- a/packages/api/src/entity/groups/group.ts +++ b/packages/api/src/entity/groups/group.ts @@ -32,4 +32,16 @@ export class Group { @OneToMany(() => GroupMembership, (groupMembership) => groupMembership.group) members!: GroupMembership[] + + @Column('text', { nullable: true }) + description?: string | null + + @Column('text', { nullable: true }) + topics?: string | null + + @Column('boolean', { default: false }) + onlyAdminCanPost!: boolean + + @Column('boolean', { default: false }) + onlyAdminCanSeeMembers!: boolean } diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 4e949c0ef..07b53580a 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -287,9 +287,13 @@ export enum CreateGroupErrorCode { } export type CreateGroupInput = { + description?: InputMaybe; expiresInDays?: InputMaybe; maxMembers?: InputMaybe; name: Scalars['String']; + onlyAdminCanPost?: InputMaybe; + onlyAdminCanSeeMembers?: InputMaybe; + topics?: InputMaybe>; }; export type CreateGroupResult = CreateGroupError | CreateGroupSuccess; @@ -1866,11 +1870,15 @@ export type Recommendation = { export type RecommendationGroup = { __typename?: 'RecommendationGroup'; admins: Array; + canPost: Scalars['Boolean']; + canSeeMembers: Scalars['Boolean']; createdAt: Scalars['Date']; + description?: Maybe; id: Scalars['ID']; inviteUrl: Scalars['String']; members: Array; name: Scalars['String']; + topics?: Maybe>; updatedAt: Scalars['Date']; }; @@ -4959,11 +4967,15 @@ export type RecommendationResolvers = { admins?: Resolver, ParentType, ContextType>; + canPost?: Resolver; + canSeeMembers?: Resolver; createdAt?: Resolver; + description?: Resolver, ParentType, ContextType>; id?: Resolver; inviteUrl?: Resolver; members?: Resolver, ParentType, ContextType>; name?: Resolver; + topics?: Resolver>, ParentType, ContextType>; updatedAt?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index a6f3f4cf7..6036c33fc 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -243,9 +243,13 @@ enum CreateGroupErrorCode { } input CreateGroupInput { + description: String expiresInDays: Int maxMembers: Int name: String! + onlyAdminCanPost: Boolean + onlyAdminCanSeeMembers: Boolean + topics: [String!] } union CreateGroupResult = CreateGroupError | CreateGroupSuccess @@ -1312,11 +1316,15 @@ type Recommendation { type RecommendationGroup { admins: [User!]! + canPost: Boolean! + canSeeMembers: Boolean! createdAt: Date! + description: String id: ID! inviteUrl: String! members: [User!]! name: String! + topics: [String!] updatedAt: Date! } diff --git a/packages/api/src/resolvers/recommendations/index.ts b/packages/api/src/resolvers/recommendations/index.ts index 18506914a..72e7c5043 100644 --- a/packages/api/src/resolvers/recommendations/index.ts +++ b/packages/api/src/resolvers/recommendations/index.ts @@ -39,6 +39,7 @@ import { In } from 'typeorm' import { getPageByParam } from '../../elastic/pages' import { enqueueRecommendation } from '../../utils/createTask' import { env } from '../../env' +import { analytics } from '../../utils/analytics' export const createGroupResolver = authorized< CreateGroupSuccess, @@ -70,6 +71,20 @@ export const createGroupResolver = authorized< name: input.name, maxMembers: input.maxMembers, expiresInDays: input.expiresInDays, + description: input.description, + topics: input.topics, + onlyAdminCanPost: input.onlyAdminCanPost, + onlyAdminCanSeeMembers: input.onlyAdminCanSeeMembers, + }) + + analytics.track({ + userId: uid, + event: 'group_created', + properties: { + group_id: group.id, + group_name: group.name, + group_invite_code: invite.code, + }, }) await createLabelAndRuleForGroup(uid, group.name) @@ -83,6 +98,10 @@ export const createGroupResolver = authorized< inviteUrl, admins: [user], members: [user], + canSeeMembers: true, + canPost: true, + description: group.description, + topics: group.topics?.split(','), }, } } catch (error) { @@ -264,6 +283,15 @@ export const joinGroupResolver = authorized< const group = await joinGroup(user, inviteCode) + analytics.track({ + userId: uid, + event: 'group_joined', + properties: { + group_id: group.id, + group_name: group.name, + }, + }) + await createLabelAndRuleForGroup(user.id, group.name) return { @@ -403,6 +431,14 @@ export const leaveGroupResolver = authorized< const success = await leaveGroup(user, groupId) + analytics.track({ + userId: uid, + event: 'group_left', + properties: { + group_id: groupId, + }, + }) + return { success, } diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 2399578a3..081d4b60f 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2158,6 +2158,10 @@ const schema = gql` name: String! @sanitize(maxLength: 140) maxMembers: Int expiresInDays: Int + description: String + topics: [String!] + onlyAdminCanPost: Boolean + onlyAdminCanSeeMembers: Boolean } union CreateGroupResult = CreateGroupSuccess | CreateGroupError @@ -2174,6 +2178,10 @@ const schema = gql` members: [User!]! createdAt: Date! updatedAt: Date! + canPost: Boolean! + description: String + topics: [String!] + canSeeMembers: Boolean! } type CreateGroupError { diff --git a/packages/api/src/services/groups.ts b/packages/api/src/services/groups.ts index 64f5d0f41..2776ef89a 100644 --- a/packages/api/src/services/groups.ts +++ b/packages/api/src/services/groups.ts @@ -17,6 +17,10 @@ export const createGroup = async (input: { name: string maxMembers?: number | null expiresInDays?: number | null + description?: string | null + topics?: string[] | null + onlyAdminCanPost?: boolean | null + onlyAdminCanSeeMembers?: boolean | null }): Promise<[Group, Invite]> => { const [group, invite] = await AppDataSource.transaction<[Group, Invite]>( async (t) => { @@ -32,6 +36,10 @@ export const createGroup = async (input: { const group = await t.getRepository(Group).save({ name: input.name, createdBy: input.admin, + description: input.description, + topics: input.topics?.join(','), + onlyAdminCanPost: input.onlyAdminCanPost ?? false, + onlyAdminCanSeeMembers: input.onlyAdminCanSeeMembers ?? false, }) const code = nanoid(8) @@ -71,13 +79,17 @@ export const getRecommendationGroups = async ( return groupMembers.map((gm) => { const admins: GraphqlUser[] = [] const members: GraphqlUser[] = [] - gm.group.members.forEach((m) => { - const user = userDataToUser(m.user) - if (m.isAdmin) { - admins.push(user) - } - members.push(user) - }) + const canSeeMembers = gm.group.onlyAdminCanSeeMembers ? gm.isAdmin : true + if (canSeeMembers) { + // Return all members + gm.group.members.forEach((m) => { + const user = userDataToUser(m.user) + if (m.isAdmin) { + admins.push(user) + } + members.push(user) + }) + } return { id: gm.group.id, @@ -87,6 +99,10 @@ export const getRecommendationGroups = async ( inviteUrl: getInviteUrl(gm.invite), admins, members, + topics: gm.group.topics?.split(','), + description: gm.group.description, + canPost: gm.group.onlyAdminCanPost ? gm.isAdmin : true, + canSeeMembers, } }) } @@ -135,19 +151,26 @@ having count(*) < $4`, }) const admins: GraphqlUser[] = [] const members: GraphqlUser[] = [] - group.members.forEach((m) => { - const user = userDataToUser(m.user) - if (m.isAdmin) { - admins.push(user) - } - members.push(user) - }) + if (!group.onlyAdminCanSeeMembers) { + // Return all members + group.members.forEach((m) => { + const user = userDataToUser(m.user) + if (m.isAdmin) { + admins.push(user) + } + members.push(user) + }) + } return { ...group, inviteUrl: getInviteUrl(invite), admins, members, + topics: group.topics?.split(','), + description: group.description, + canPost: !group.onlyAdminCanPost, + canSeeMembers: !group.onlyAdminCanSeeMembers, } } diff --git a/packages/db/migrations/0104.do.add_description_to_group.sql b/packages/db/migrations/0104.do.add_description_to_group.sql new file mode 100755 index 000000000..e2563e61f --- /dev/null +++ b/packages/db/migrations/0104.do.add_description_to_group.sql @@ -0,0 +1,13 @@ +-- Type: DO +-- Name: add_description_to_group +-- Description: Add description, topics, only_admin_can_post, only_admin_can_see_members to group table + +BEGIN; + +ALTER TABLE omnivore."group" + ADD COLUMN description text, + ADD COLUMN topics text, + ADD COLUMN only_admin_can_post boolean NOT NULL DEFAULT false, + ADD COLUMN only_admin_can_see_members boolean NOT NULL DEFAULT false; + +COMMIT; diff --git a/packages/db/migrations/0104.undo.add_description_to_group.sql b/packages/db/migrations/0104.undo.add_description_to_group.sql new file mode 100755 index 000000000..8fa383de6 --- /dev/null +++ b/packages/db/migrations/0104.undo.add_description_to_group.sql @@ -0,0 +1,13 @@ +-- Type: UNDO +-- Name: add_description_to_group +-- Description: Add description, topics, only_admin_can_post, only_admin_can_see_members to group table + +BEGIN; + +ALTER TABLE omnivore."group" + DROP COLUMN IF EXISTS description, + DROP COLUMN IF EXISTS topics, + DROP COLUMN IF EXISTS only_admin_can_post, + DROP COLUMN IF EXISTS only_admin_can_see_members; + +COMMIT;