diff --git a/libs/gql-schema/organization-settings.ts b/libs/gql-schema/organization-settings.ts index b3110fca4..326e4b2d4 100644 --- a/libs/gql-schema/organization-settings.ts +++ b/libs/gql-schema/organization-settings.ts @@ -13,6 +13,7 @@ export const schema = ` showDoNotAssignMessage: Boolean doNotAssignMessage: String defaultAutosendingControlsMode: AutosendingControlsMode + maxSmsSegmentLength: Int # Superadmin startCampaignRequiresApproval: Boolean @@ -28,6 +29,7 @@ export const schema = ` confirmationClickForScriptLinks: Boolean! showDoNotAssignMessage: Boolean! doNotAssignMessage: String! + maxSmsSegmentLength: Int # Supervolunteer startCampaignRequiresApproval: Boolean diff --git a/libs/spoke-codegen/src/graphql/general-settings.graphql b/libs/spoke-codegen/src/graphql/general-settings.graphql index 96a20c03e..bd2cf064c 100644 --- a/libs/spoke-codegen/src/graphql/general-settings.graphql +++ b/libs/spoke-codegen/src/graphql/general-settings.graphql @@ -76,3 +76,26 @@ mutation UpdateAutosendingSettings( defaultAutosendingControlsMode } } + +query GetMessageSendingSettings($organizationId: String!) { + organization(id: $organizationId) { + id + settings { + id + maxSmsSegmentLength + } + } +} + +mutation UpdateMessageSendingSettings( + $organizationId: String! + $maxSmsSegmentLength: Int +) { + editOrganizationSettings( + id: $organizationId + input: { maxSmsSegmentLength: $maxSmsSegmentLength } + ) { + id + maxSmsSegmentLength + } +} diff --git a/libs/spoke-codegen/src/graphql/spoke-context.graphql b/libs/spoke-codegen/src/graphql/spoke-context.graphql index 61f05a4a9..ccdc523c8 100644 --- a/libs/spoke-codegen/src/graphql/spoke-context.graphql +++ b/libs/spoke-codegen/src/graphql/spoke-context.graphql @@ -20,4 +20,5 @@ fragment OrganizationSettingsInfo on OrganizationSettings { scriptPreviewForSupervolunteers defaultCampaignBuilderMode defaultAutosendingControlsMode + maxSmsSegmentLength } diff --git a/src/containers/Settings/components/General.jsx b/src/containers/Settings/components/General.jsx index d6c6a93da..5f55b990b 100644 --- a/src/containers/Settings/components/General.jsx +++ b/src/containers/Settings/components/General.jsx @@ -25,6 +25,7 @@ import { loadData } from "../../hoc/with-operations"; import AutosendingSettingsCard from "./AutosendingSettingsCard"; import CampaignBuilderSettingsCard from "./CampaignBuilderSettingsCard"; import EditName from "./EditName"; +import MessageSendingSettingsCard from "./MessageSendingSettingsCard"; import RejectedTextersMessageCard from "./RejectedTextersMessageCard"; import Review10DlcInfo from "./Review10DlcInfo"; import ScriptPreviewSettingsCard from "./ScriptPreviewSettingsCard"; @@ -473,6 +474,11 @@ class Settings extends React.Component { style={{ marginBottom: 20 }} /> + + {window.ENABLE_TROLLBOT && ( = ( + props +) => { + const { organizationId, style } = props; + + const getSettingsState = useGetMessageSendingSettingsQuery({ + variables: { organizationId } + }); + const settings = getSettingsState?.data?.organization?.settings; + + const [ + setMaxSmsSegmentLength, + updateState + ] = useUpdateMessageSendingSettingsMutation(); + + const working = getSettingsState.loading || updateState.loading; + const errorMsg = + getSettingsState.error?.message ?? updateState.error?.message; + + const maxSmsSegmentLength = settings?.maxSmsSegmentLength; + const showMaxSmsSegmentLength = maxSmsSegmentLength !== null; + + const setNewMaxSmsSegmentLength = async ( + newMax: number | null | undefined + ) => { + await setMaxSmsSegmentLength({ + variables: { + organizationId, + maxSmsSegmentLength: newMax + } + }); + }; + + // eslint-disable-next-line max-len + const handleChangeMaxSmsSegmentLength: React.ChangeEventHandler = async ( + event + ) => { + if (working) return; + const newMax = event.target.valueAsNumber; + await setNewMaxSmsSegmentLength(newMax); + }; + + // eslint-disable-next-line max-len + const handleToggleMmsConversion: React.ChangeEventHandler = async ( + event + ) => { + const { checked } = event.target; + const DEFAULT_MAX_SMS_SEGMENT_LENGTH = 3; + const newMax = checked ? DEFAULT_MAX_SMS_SEGMENT_LENGTH : null; + await setNewMaxSmsSegmentLength(newMax); + }; + + return ( + + + + {errorMsg && Error: {errorMsg}} +

+ Turn on this feature to automatically convert long SMS messages to + MMS. If turned on, SMS messages longer than the length you set + will be converted. For example, if you set the max length to 3, a 4 + segment SMS message will be converted to MMS. +

+

+ Messages longer than 3 segments are usually cheaper to send as MMS. + You may notice changes in deliverability when switching from SMS to + MMS messages.{" "} + + Learn more about sending MMS here + +

+ + } + /> + {showMaxSmsSegmentLength && ( + + )} +
+
+ ); +}; + +export default MessageSendingSettingsCard; diff --git a/src/schema.graphql b/src/schema.graphql index ea14a2ca9..a11696ae1 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -482,6 +482,7 @@ input OrganizationSettingsInput { showDoNotAssignMessage: Boolean doNotAssignMessage: String defaultAutosendingControlsMode: AutosendingControlsMode + maxSmsSegmentLength: Int # Superadmin startCampaignRequiresApproval: Boolean @@ -497,6 +498,7 @@ type OrganizationSettings { confirmationClickForScriptLinks: Boolean! showDoNotAssignMessage: Boolean! doNotAssignMessage: String! + maxSmsSegmentLength: Int # Supervolunteer startCampaignRequiresApproval: Boolean diff --git a/src/server/api/lib/assemble-numbers.ts b/src/server/api/lib/assemble-numbers.ts index 40b00e8ed..a6aa9bd9d 100644 --- a/src/server/api/lib/assemble-numbers.ts +++ b/src/server/api/lib/assemble-numbers.ts @@ -1,5 +1,6 @@ import type { Knex } from "knex"; import type { PoolClient } from "pg"; +import { getSpokeCharCount } from "src/lib/charset-utils"; import { config } from "../../../config"; import { getFormattedPhoneNumber } from "../../../lib/phone-format"; @@ -177,8 +178,24 @@ export const sendMessage = async ( .reader("campaign_contact") .where({ id: campaignContactId }) .first("zip"); + + const { maxSmsSegmentLength } = await r + .reader("organization") + .where({ id: organizationId }) + .first("features") + .then(({ features }: { features: string }) => JSON.parse(features)) + .catch(() => ({})); + const { body, mediaUrl } = messageComponents(messageText); - const mediaUrls = mediaUrl ? [mediaUrl] : undefined; + const { msgCount } = getSpokeCharCount(messageText); + + const mediaUrls = mediaUrl + ? [mediaUrl] + : // format for switchboard to send empty MMS + maxSmsSegmentLength && msgCount > maxSmsSegmentLength + ? [] + : undefined; + const messageInput: NumbersOutboundMessagePayload = { profileId, to, @@ -188,6 +205,8 @@ export const sendMessage = async ( contactZipCode: contactZipCode === "" ? null : contactZipCode }; + console.log(messageInput); + try { const result = await numbers.sms.sendMessage(messageInput); const { data, errors } = result; diff --git a/src/server/api/organization-settings.ts b/src/server/api/organization-settings.ts index 5f0f21205..7245703ad 100644 --- a/src/server/api/organization-settings.ts +++ b/src/server/api/organization-settings.ts @@ -28,6 +28,7 @@ interface IOrganizationSettings { doNotAssignMessage: string; defaultCampaignBuilderMode: CampaignBuilderMode; defaultAutosendingControlsMode: AutosendingControlsMode; + maxSmsSegmentLength: number | null; } const SETTINGS_PERMISSIONS: { @@ -45,7 +46,8 @@ const SETTINGS_PERMISSIONS: { defaultAutosendingControlsMode: UserRoleType.ADMIN, defaulTexterApprovalStatus: UserRoleType.OWNER, numbersApiKey: UserRoleType.OWNER, - trollbotWebhookUrl: UserRoleType.OWNER + trollbotWebhookUrl: UserRoleType.OWNER, + maxSmsSegmentLength: UserRoleType.TEXTER }; const SETTINGS_WRITE_PERMISSIONS: { @@ -63,7 +65,8 @@ const SETTINGS_WRITE_PERMISSIONS: { showDoNotAssignMessage: UserRoleType.OWNER, doNotAssignMessage: UserRoleType.OWNER, defaultAutosendingControlsMode: UserRoleType.OWNER, - startCampaignRequiresApproval: UserRoleType.SUPERADMIN + startCampaignRequiresApproval: UserRoleType.SUPERADMIN, + maxSmsSegmentLength: UserRoleType.OWNER }; const SETTINGS_NAMES: Partial< @@ -86,7 +89,8 @@ const SETTINGS_DEFAULTS: IOrganizationSettings = { doNotAssignMessage: "Your ability to request texts has been put on hold. Please a contact a text team leader for more information.", defaultCampaignBuilderMode: CampaignBuilderMode.Advanced, - defaultAutosendingControlsMode: AutosendingControlsMode.Detailed + defaultAutosendingControlsMode: AutosendingControlsMode.Detailed, + maxSmsSegmentLength: 3 }; const SETTINGS_TRANSFORMERS: Partial< @@ -133,13 +137,17 @@ export const getOrgFeature = ( const finalName = SETTINGS_NAMES[featureName] ?? featureName; try { const features = JSON.parse(rawFeatures); - const value = features[finalName] ?? defaultValue ?? null; + + const foundValue = features[finalName]; + const returnValue = + foundValue === undefined ? defaultValue ?? null : foundValue; + const transformer = SETTINGS_TRANSFORMERS[featureName]; - if (transformer && value) { - const result = transformer(value); + if (transformer && returnValue) { + const result = transformer(returnValue); return result as IOrganizationSettings[T]; } - return value; + return returnValue; } catch (_err) { return SETTINGS_DEFAULTS[featureName] ?? null; } @@ -183,7 +191,8 @@ export const resolvers = { "defaultCampaignBuilderMode", "showDoNotAssignMessage", "doNotAssignMessage", - "defaultAutosendingControlsMode" + "defaultAutosendingControlsMode", + "maxSmsSegmentLength" ]) } }; diff --git a/src/server/organization-settings.spec.ts b/src/server/organization-settings.spec.ts index ae9337ccc..73400714b 100644 --- a/src/server/organization-settings.spec.ts +++ b/src/server/organization-settings.spec.ts @@ -37,7 +37,8 @@ describe("get organization settings", () => { defaultAutosendingControlsMode: AutosendingControlsMode.Basic, defaulTexterApprovalStatus: RequestAutoApproveType.APPROVAL_REQUIRED, numbersApiKey: "SomethingSecret", - trollbotWebhookUrl: "https://rewired.coop/trolls" + trollbotWebhookUrl: "https://rewired.coop/trolls", + maxSmsSegmentLength: 3 }; const makeSettingsRequest = async ( @@ -68,6 +69,7 @@ describe("get organization settings", () => { defaulTexterApprovalStatus numbersApiKey trollbotWebhookUrl + maxSmsSegmentLength } } } @@ -135,6 +137,7 @@ describe("get organization settings", () => { ); expect(settings.numbersApiKey).not.toBeNull(); expect(settings.trollbotWebhookUrl).toEqual(features.trollbotWebhookUrl); + expect(settings.maxSmsSegmentLength).toEqual(features.maxSmsSegmentLength); }); it("returns the correct role required", () => {