diff --git a/data/db/datasource.ts b/data/db/datasource.ts index 825e22298..a9c3c866e 100644 --- a/data/db/datasource.ts +++ b/data/db/datasource.ts @@ -38,6 +38,7 @@ import { AddIndexToSent1712656017130 } from "./migrations/1712656017130-addIndex import { RemoveSentViaConverse1717625558678 } from "./migrations/1717625558678-RemoveSentViaConverse"; import { AddSuperAdmin1717631723249 } from "./migrations/1717631723249-AddSuperAdmin"; import { AddIsActive1721143963530 } from "./migrations/1721143963530-addIsActive"; +import { AddGroupCreatorAndAddedBy1726807503232 } from "./migrations/1726807503232-AddGroupCreatorAndAddedBy"; import { RemoveProfile1726828413530 } from "./migrations/1726828413530-removeProfileDb"; // We now use built in SQLite v3.45.1 from op-sqlite @@ -99,6 +100,7 @@ export const getDataSource = async (account: string) => { RemoveSentViaConverse1717625558678, AddSuperAdmin1717631723249, AddIsActive1721143963530, + AddGroupCreatorAndAddedBy1726807503232, RemoveProfile1726828413530, ], type: "react-native", diff --git a/data/db/entities/conversationEntity.ts b/data/db/entities/conversationEntity.ts index 13aa0e3c5..7833b7bb8 100644 --- a/data/db/entities/conversationEntity.ts +++ b/data/db/entities/conversationEntity.ts @@ -46,6 +46,12 @@ export class Conversation { @Column("simple-array", { nullable: true }) groupMembers?: string[]; + @Column("text", { nullable: true }) + groupCreator?: string; + + @Column("text", { nullable: true }) + groupAddedBy?: string; + @Column("boolean", { nullable: true }) isActive?: boolean; diff --git a/data/db/migrations/1726807503232-AddGroupCreatorAndAddedBy.ts b/data/db/migrations/1726807503232-AddGroupCreatorAndAddedBy.ts new file mode 100644 index 000000000..763773279 --- /dev/null +++ b/data/db/migrations/1726807503232-AddGroupCreatorAndAddedBy.ts @@ -0,0 +1,49 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddGroupCreatorAndAddedBy1726807503232 + implements MigrationInterface +{ + name = "AddGroupCreatorAndAddedBy1726807503232"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_b895a35679cdf0702fb2218718"`); + await queryRunner.query(`DROP INDEX "IDX_134e668e0d62c7cd93624ea4d5"`); + await queryRunner.query( + `CREATE TABLE "temporary_conversation" ("topic" text PRIMARY KEY NOT NULL, "peerAddress" text, "createdAt" integer NOT NULL, "contextConversationId" text, "contextMetadata" text, "readUntil" integer NOT NULL DEFAULT (0), "pending" boolean NOT NULL DEFAULT (0), "version" text NOT NULL DEFAULT ('v2'), "spamScore" decimal(6,2), "isGroup" boolean NOT NULL DEFAULT (0), "groupMembers" text, "groupAdmins" text, "groupPermissionLevel" text, "lastNotificationsSubscribedPeriod" integer, "groupSuperAdmins" text, "groupName" text, "isActive" boolean, "groupCreator" text, "groupAddedBy" text)` + ); + await queryRunner.query( + `INSERT INTO "temporary_conversation"("topic", "peerAddress", "createdAt", "contextConversationId", "contextMetadata", "readUntil", "pending", "version", "spamScore", "isGroup", "groupMembers", "groupAdmins", "groupPermissionLevel", "lastNotificationsSubscribedPeriod", "groupSuperAdmins", "groupName", "isActive") SELECT "topic", "peerAddress", "createdAt", "contextConversationId", "contextMetadata", "readUntil", "pending", "version", "spamScore", "isGroup", "groupMembers", "groupAdmins", "groupPermissionLevel", "lastNotificationsSubscribedPeriod", "groupSuperAdmins", "groupName", "isActive" FROM "conversation"` + ); + await queryRunner.query(`DROP TABLE "conversation"`); + await queryRunner.query( + `ALTER TABLE "temporary_conversation" RENAME TO "conversation"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_b895a35679cdf0702fb2218718" ON "conversation" ("peerAddress") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_134e668e0d62c7cd93624ea4d5" ON "conversation" ("pending") ` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_134e668e0d62c7cd93624ea4d5"`); + await queryRunner.query(`DROP INDEX "IDX_b895a35679cdf0702fb2218718"`); + await queryRunner.query( + `ALTER TABLE "conversation" RENAME TO "temporary_conversation"` + ); + await queryRunner.query( + `CREATE TABLE "conversation" ("topic" text PRIMARY KEY NOT NULL, "peerAddress" text, "createdAt" integer NOT NULL, "contextConversationId" text, "contextMetadata" text, "readUntil" integer NOT NULL DEFAULT (0), "pending" boolean NOT NULL DEFAULT (0), "version" text NOT NULL DEFAULT ('v2'), "spamScore" decimal(6,2), "isGroup" boolean NOT NULL DEFAULT (0), "groupMembers" text, "groupAdmins" text, "groupPermissionLevel" text, "lastNotificationsSubscribedPeriod" integer, "groupSuperAdmins" text, "groupName" text, "isActive" boolean)` + ); + await queryRunner.query( + `INSERT INTO "conversation"("topic", "peerAddress", "createdAt", "contextConversationId", "contextMetadata", "readUntil", "pending", "version", "spamScore", "isGroup", "groupMembers", "groupAdmins", "groupPermissionLevel", "lastNotificationsSubscribedPeriod", "groupSuperAdmins", "groupName", "isActive") SELECT "topic", "peerAddress", "createdAt", "contextConversationId", "contextMetadata", "readUntil", "pending", "version", "spamScore", "isGroup", "groupMembers", "groupAdmins", "groupPermissionLevel", "lastNotificationsSubscribedPeriod", "groupSuperAdmins", "groupName", "isActive" FROM "temporary_conversation"` + ); + await queryRunner.query(`DROP TABLE "temporary_conversation"`); + await queryRunner.query( + `CREATE INDEX "IDX_134e668e0d62c7cd93624ea4d5" ON "conversation" ("pending") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_b895a35679cdf0702fb2218718" ON "conversation" ("peerAddress") ` + ); + } +} diff --git a/data/helpers/conversations/spamScore.ts b/data/helpers/conversations/spamScore.ts index ad20ecce8..6b1b171f9 100644 --- a/data/helpers/conversations/spamScore.ts +++ b/data/helpers/conversations/spamScore.ts @@ -48,11 +48,8 @@ export const saveSpamScores = async ( export const refreshAllSpamScores = async (account: string) => { const { conversations } = getChatStore(account).getState(); const conversationsToScore = Object.values(conversations).filter( - (c) => - c.messagesIds.length && - (c.spamScore === undefined || c.spamScore === null) + (c) => c.spamScore === undefined || c.spamScore === null ); - if (conversationsToScore.length === 0) return; await computeConversationsSpamScores(account, conversationsToScore); }; @@ -61,21 +58,30 @@ export const computeConversationsSpamScores = async ( account: string, conversations: XmtpConversationWithUpdate[] ) => { - // @todo => spam score for group convos?? - const conversationsPeerAddresses = new Set( + const conversationsRequesterAddresses = new Set( conversations - .filter((c) => !!c.peerAddress) - .map((c) => c.peerAddress as string) + .filter((c) => !!c.peerAddress || !!c.groupAddedBy) + .map((c) => (c.isGroup ? c.groupAddedBy : (c.peerAddress as string))) + .filter((address): address is string => address !== undefined) ); const sendersSpamScores = await getSendersSpamScores( - Array.from(conversationsPeerAddresses) + Array.from(conversationsRequesterAddresses) ); const topicSpamScores: TopicSpamScores = {}; conversations.forEach((conversation) => { - if (!conversation.peerAddress) return; - const senderSpamScore = sendersSpamScores[conversation.peerAddress]; - if (!conversation.messagesIds.length && senderSpamScore) { + if (!(conversation.peerAddress || conversation.groupAddedBy)) return; + + const senderKey = conversation.isGroup + ? conversation.groupAddedBy + : conversation.peerAddress; + if (!senderKey) return; + + const senderSpamScore = sendersSpamScores[senderKey]; + if ( + !conversation.messagesIds.length && + typeof senderSpamScore === "number" + ) { // Cannot score an empty conversation further, score is just the // sender spam score topicSpamScores[conversation.topic] = senderSpamScore; diff --git a/data/helpers/conversations/upsertConversations.ts b/data/helpers/conversations/upsertConversations.ts index bf400f7e2..65e836e85 100644 --- a/data/helpers/conversations/upsertConversations.ts +++ b/data/helpers/conversations/upsertConversations.ts @@ -2,6 +2,7 @@ import logger from "@utils/logger"; import { In } from "typeorm/browser"; import { upgradePendingConversationsIfNeeded } from "./pendingConversations"; +import { computeConversationsSpamScores } from "./spamScore"; import { navigateToTopicWithRetry, topicToNavigateTo, @@ -12,7 +13,10 @@ import { Conversation } from "../../db/entities/conversationEntity"; import { upsertRepository } from "../../db/upsert"; import { xmtpConversationToDb } from "../../mappers"; import { getChatStore } from "../../store/accountsStore"; -import { XmtpConversation } from "../../store/chatStore"; +import { + XmtpConversation, + XmtpConversationWithUpdate, +} from "../../store/chatStore"; import { refreshProfilesIfNeeded } from "../profiles/profilesUpdate"; export const saveConversations = async ( @@ -79,6 +83,17 @@ const setupAndSaveConversations = async ( const alreadyConversationInDbWithTopic = alreadyConversationsByTopic[conversation.topic]; + // If spam score is not computed, compute it + if ( + conversation.spamScore === undefined || + conversation.spamScore === null + ) { + logger.debug("Empty spam score, computing..."); + computeConversationsSpamScores(account, [ + conversation as XmtpConversationWithUpdate, + ]); + } + conversation.readUntil = conversation.readUntil || alreadyConversationInDbWithTopic?.readUntil || diff --git a/data/mappers.ts b/data/mappers.ts index 29f4a6cbf..5514c3fa8 100644 --- a/data/mappers.ts +++ b/data/mappers.ts @@ -82,6 +82,8 @@ export const xmtpConversationToDb = ( isGroup: xmtpConversation.isGroup, isActive: xmtpConversation.isGroup ? !!xmtpConversation.isActive : true, groupMembers: xmtpConversation.groupMembers, + groupCreator: xmtpConversation.groupCreator, + groupAddedBy: xmtpConversation.groupAddedBy, groupName: xmtpConversation.isGroup ? xmtpConversation.groupName : undefined, groupPermissionLevel: xmtpConversation.groupPermissionLevel, lastNotificationsSubscribedPeriod: @@ -131,6 +133,8 @@ export const xmtpConversationFromDb = ( isGroup: dbConversation.isGroup, isActive: dbConversation.isGroup ? !!dbConversation.isActive : true, groupMembers, + groupCreator: dbConversation.groupCreator, + groupAddedBy: dbConversation.groupAddedBy, groupAdmins: dbConversation.groupAdmins, groupName: dbConversation.groupName, groupPermissionLevel: dbConversation.groupPermissionLevel, diff --git a/data/store/chatStore.ts b/data/store/chatStore.ts index fbc825c08..39e74df98 100644 --- a/data/store/chatStore.ts +++ b/data/store/chatStore.ts @@ -55,6 +55,8 @@ export type XmtpDMConversation = XmtpConversationShared & { groupMembers?: undefined; groupAdmins?: undefined; groupPermissionLevel?: undefined; + groupCreator?: undefined; + groupAddedBy?: undefined; }; export type XmtpGroupConversation = XmtpConversationShared & { diff --git a/i18n/translations/en.ts b/i18n/translations/en.ts index 1e02d9a7f..110b54946 100644 --- a/i18n/translations/en.ts +++ b/i18n/translations/en.ts @@ -183,6 +183,8 @@ const en = { hidden_requests: "Hidden requests", suggestion_text: "Based on your onchain history, we've made some suggestions on who you may know.", + no_suggestions_text: + "You currently have no message requests. We'll make suggestions here as you connect with others.", hidden_requests_warn: "Requests containing messages that may be offensive or unwanted are moved to this folder.", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 099a59268..1a295a949 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2418,7 +2418,7 @@ SPEC CHECKSUMS: web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 XMTP: a9e7382ec5b57eeda3df7b177f034d061e3c9b61 XMTPReactNative: 63c46d6ba9b8dc5831ea49e8716f4b60cc012651 - Yoga: 1ab23c1835475da69cf14e211a560e73aab24cb0 + Yoga: 33622183a85805e12703cd618b2c16bfd18bfffb PODFILE CHECKSUM: beb6a8b55061fc0f811288dc66c75239474eba01 diff --git a/screens/Navigation/ConversationRequestsListNav.ios.tsx b/screens/Navigation/ConversationRequestsListNav.ios.tsx index 067084b1b..c2c116815 100644 --- a/screens/Navigation/ConversationRequestsListNav.ios.tsx +++ b/screens/Navigation/ConversationRequestsListNav.ios.tsx @@ -132,51 +132,44 @@ export default function ConversationRequestsListNav() { const hasLikelyNotSpam = likelyNotSpam.length > 0; const hasSpam = likelySpam.length > 0; - const hasBothTypesOfRequests = hasLikelyNotSpam && hasSpam; + const hasBothTypesOfRequests = true; const handleSegmentChange = (index: number) => { setSelectedSegment(index); }; const renderSegmentedController = () => { - if (hasBothTypesOfRequests) { - return ( - - ); - } - return null; + return ( + + ); }; const renderContent = (navigationProps: { route: RouteProp; navigation: NativeStackNavigationProp; }) => { - const showSuggestionText = - (hasBothTypesOfRequests && selectedSegment === 0) || - (!hasBothTypesOfRequests && hasLikelyNotSpam); - const showSpamWarning = - (hasBothTypesOfRequests && selectedSegment === 1) || - (!hasBothTypesOfRequests && hasSpam); - const itemsToShow = hasBothTypesOfRequests - ? selectedSegment === 0 - ? likelyNotSpam - : likelySpam - : hasLikelyNotSpam - ? likelyNotSpam - : likelySpam; + const showSuggestionText = selectedSegment === 0 && hasLikelyNotSpam; + const showNoSuggestionsText = selectedSegment === 0 && !hasLikelyNotSpam; + const showSpamWarning = selectedSegment === 1; + const itemsToShow = selectedSegment === 0 ? likelyNotSpam : likelySpam; return ( <> - {hasBothTypesOfRequests && renderSegmentedController()} + {renderSegmentedController()} {showSuggestionText && ( {translate("suggestion_text")} )} + {showNoSuggestionsText && ( + + {translate("no_suggestions_text")} + + )} {showSpamWarning && ( {translate("hidden_requests_warn")} diff --git a/scripts/migrations/converse-sample.sqlite b/scripts/migrations/converse-sample.sqlite index f4c81b10b..96092e703 100644 Binary files a/scripts/migrations/converse-sample.sqlite and b/scripts/migrations/converse-sample.sqlite differ diff --git a/scripts/migrations/datasource.ts b/scripts/migrations/datasource.ts index 7862abc3a..675bc6a9a 100644 --- a/scripts/migrations/datasource.ts +++ b/scripts/migrations/datasource.ts @@ -32,6 +32,7 @@ import { AddIndexToSent1712656017130 } from "../../data/db/migrations/1712656017 import { RemoveSentViaConverse1717625558678 } from "../../data/db/migrations/1717625558678-RemoveSentViaConverse"; import { AddSuperAdmin1717631723249 } from "../../data/db/migrations/1717631723249-AddSuperAdmin"; import { AddIsActive1721143963530 } from "../../data/db/migrations/1721143963530-addIsActive"; +import { AddGroupCreatorAndAddedBy1726807503232 } from "../../data/db/migrations/1726807503232-AddGroupCreatorAndAddedBy"; import { RemoveProfile1726828413530 } from "../../data/db/migrations/1726828413530-removeProfileDb"; const dataSource = new DataSource({ @@ -67,6 +68,7 @@ const dataSource = new DataSource({ RemoveSentViaConverse1717625558678, AddSuperAdmin1717631723249, AddIsActive1721143963530, + AddGroupCreatorAndAddedBy1726807503232, RemoveProfile1726828413530, ], type: "sqlite", diff --git a/scripts/migrations/db.ts b/scripts/migrations/db.ts index 09eb4d97a..67d940f42 100644 --- a/scripts/migrations/db.ts +++ b/scripts/migrations/db.ts @@ -110,6 +110,8 @@ const commands = { groupMembers: [peerAddress, myAddress], groupAdmins: [myAddress], groupSuperAdmins: [myAddress], + groupCreator: myAddress, + groupAddedBy: myAddress, }); for (let messageIndex = 0; messageIndex < 10; messageIndex++) { diff --git a/scripts/migrations/entities/conversationEntity.ts b/scripts/migrations/entities/conversationEntity.ts index a1ce64913..2ce1d342f 100644 --- a/scripts/migrations/entities/conversationEntity.ts +++ b/scripts/migrations/entities/conversationEntity.ts @@ -37,6 +37,12 @@ export class Conversation { @Column("simple-array", { nullable: true }) groupMembers?: string[]; + @Column("text", { nullable: true }) + groupCreator?: string; + + @Column("text", { nullable: true }) + groupAddedBy?: string; + @Column("boolean", { nullable: true }) isActive?: boolean; diff --git a/utils/xmtpRN/conversations.ts b/utils/xmtpRN/conversations.ts index c08b47f0c..d8e552709 100644 --- a/utils/xmtpRN/conversations.ts +++ b/utils/xmtpRN/conversations.ts @@ -804,7 +804,9 @@ export const sortRequestsBySpamScore = ( requests.forEach((conversation) => { const isLikelyNotSpam = conversation.spamScore !== undefined && - (conversation.spamScore === null || conversation.spamScore < 1); + (conversation.spamScore === null || conversation.spamScore < 1) && + conversation.version !== ConversationVersion.GROUP; + // @todo => remove this once we have group-specific spam scores if (isLikelyNotSpam) { result.likelyNotSpam.push(conversation);