Skip to content

Commit

Permalink
feat: support sending mms without media
Browse files Browse the repository at this point in the history
  • Loading branch information
ajohn25 committed Feb 14, 2024
1 parent 9f97f8d commit 2218dff
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 10 deletions.
2 changes: 2 additions & 0 deletions libs/gql-schema/organization-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const schema = `
showDoNotAssignMessage: Boolean
doNotAssignMessage: String
defaultAutosendingControlsMode: AutosendingControlsMode
maxSmsSegmentLength: Int
# Superadmin
startCampaignRequiresApproval: Boolean
Expand All @@ -28,6 +29,7 @@ export const schema = `
confirmationClickForScriptLinks: Boolean!
showDoNotAssignMessage: Boolean!
doNotAssignMessage: String!
maxSmsSegmentLength: Int
# Supervolunteer
startCampaignRequiresApproval: Boolean
Expand Down
23 changes: 23 additions & 0 deletions libs/spoke-codegen/src/graphql/general-settings.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
1 change: 1 addition & 0 deletions libs/spoke-codegen/src/graphql/spoke-context.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ fragment OrganizationSettingsInfo on OrganizationSettings {
scriptPreviewForSupervolunteers
defaultCampaignBuilderMode
defaultAutosendingControlsMode
maxSmsSegmentLength
}
6 changes: 6 additions & 0 deletions src/containers/Settings/components/General.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -473,6 +474,11 @@ class Settings extends React.Component {
style={{ marginBottom: 20 }}
/>

<MessageSendingSettingsCard
organizationId={organization.id}
style={{ marginBottom: 20 }}
/>

{window.ENABLE_TROLLBOT && (
<Card className={css(styles.sectionCard)}>
<GSForm
Expand Down
117 changes: 117 additions & 0 deletions src/containers/Settings/components/MessageSendingSettingsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import CardHeader from "@material-ui/core/CardHeader";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import TextField from "@material-ui/core/TextField";
import Alert from "@material-ui/lab/Alert";
import {
useGetMessageSendingSettingsQuery,
useUpdateMessageSendingSettingsMutation
} from "@spoke/spoke-codegen";
import React from "react";

export interface MessageSendingSettingsCardProps {
organizationId: string;
style?: React.CSSProperties;
}

// eslint-disable-next-line max-len
export const MessageSendingSettingsCard: React.FC<MessageSendingSettingsCardProps> = (
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<HTMLInputElement> = async (
event
) => {
if (working) return;
const newMax = event.target.valueAsNumber;
await setNewMaxSmsSegmentLength(newMax);
};

// eslint-disable-next-line max-len
const handleToggleMmsConversion: React.ChangeEventHandler<HTMLInputElement> = 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 (
<Card style={style}>
<CardHeader title="Message Sending Settings" disableTypography />
<CardContent>
{errorMsg && <Alert severity="error">Error: {errorMsg}</Alert>}
<p>
Turn on this feature to automatically convert long SMS messages to
MMS. If turned on, SMS messages <i>longer</i> 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.
</p>
<p style={{ marginBottom: 25 }}>
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.{" "}
<a
href="https://docs.spokerewired.com/article/86-include-an-image-in-a-message"
target="_blank"
rel="noopener noreferrer"
>
Learn more about sending MMS here
</a>
</p>
<FormControlLabel
label="Convert long SMS messages to MMS?"
control={
<Switch
checked={showMaxSmsSegmentLength}
onChange={handleToggleMmsConversion}
/>
}
/>
{showMaxSmsSegmentLength && (
<TextField
label="Max SMS Segment Length"
type="number"
value={settings?.maxSmsSegmentLength}
onChange={handleChangeMaxSmsSegmentLength}
/>
)}
</CardContent>
</Card>
);
};

export default MessageSendingSettingsCard;
2 changes: 2 additions & 0 deletions src/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ input OrganizationSettingsInput {
showDoNotAssignMessage: Boolean
doNotAssignMessage: String
defaultAutosendingControlsMode: AutosendingControlsMode
maxSmsSegmentLength: Int

# Superadmin
startCampaignRequiresApproval: Boolean
Expand All @@ -497,6 +498,7 @@ type OrganizationSettings {
confirmationClickForScriptLinks: Boolean!
showDoNotAssignMessage: Boolean!
doNotAssignMessage: String!
maxSmsSegmentLength: Int

# Supervolunteer
startCampaignRequiresApproval: Boolean
Expand Down
21 changes: 20 additions & 1 deletion src/server/api/lib/assemble-numbers.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down
25 changes: 17 additions & 8 deletions src/server/api/organization-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface IOrganizationSettings {
doNotAssignMessage: string;
defaultCampaignBuilderMode: CampaignBuilderMode;
defaultAutosendingControlsMode: AutosendingControlsMode;
maxSmsSegmentLength: number | null;
}

const SETTINGS_PERMISSIONS: {
Expand All @@ -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: {
Expand All @@ -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<
Expand All @@ -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<
Expand Down Expand Up @@ -133,13 +137,17 @@ export const getOrgFeature = <T extends keyof IOrganizationSettings>(
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;
}
Expand Down Expand Up @@ -183,7 +191,8 @@ export const resolvers = {
"defaultCampaignBuilderMode",
"showDoNotAssignMessage",
"doNotAssignMessage",
"defaultAutosendingControlsMode"
"defaultAutosendingControlsMode",
"maxSmsSegmentLength"
])
}
};
Expand Down
5 changes: 4 additions & 1 deletion src/server/organization-settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -68,6 +69,7 @@ describe("get organization settings", () => {
defaulTexterApprovalStatus
numbersApiKey
trollbotWebhookUrl
maxSmsSegmentLength
}
}
}
Expand Down Expand Up @@ -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", () => {
Expand Down

0 comments on commit 2218dff

Please sign in to comment.