Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/feat-polls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Add MSC3381 polls: create, vote on, and end polls directly in rooms (opt-in via `features.polls` in config.json).
7 changes: 3 additions & 4 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
"homeserverList": ["matrix.org", "mozilla.org", "unredacted.org", "sable.moe", "kendama.moe"],
"allowCustomHomeservers": true,
"elementCallUrl": null,

"disableAccountSwitcher": false,
"hideUsernamePasswordFields": false,

"pushNotificationDetails": {
"pushNotifyUrl": "https://sygnal.sable.moe/_matrix/push/v1/notify",
"vapidPublicKey": "BCnS4SbHjeOaqVFW4wjt5xDt_pYIL62qMzKePfYF9fl9PQU14RieIaObh7nLR_9dQf4sykZa-CTrcjkgMIE1mcg",
Expand All @@ -18,7 +16,6 @@
"slidingSync": {
"enabled": true
},

"featuredCommunities": {
"openAsDefault": false,
"spaces": [
Expand All @@ -39,9 +36,11 @@
],
"servers": ["matrixrooms.info", "mozilla.org", "unredacted.org"]
},

"hashRouter": {
"enabled": false,
"basename": "/"
},
"features": {
"polls": false
}
}
67 changes: 57 additions & 10 deletions src/app/features/room/RoomInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
import { usePowerLevelsContext } from '$hooks/usePowerLevels';
import { useRoomCreators } from '$hooks/useRoomCreators';
import { useRoomPermissions } from '$hooks/useRoomPermissions';
import { useClientConfig } from '$hooks/useClientConfig';
import { AutocompleteNotice } from '$components/editor/autocomplete/AutocompleteNotice';
import {
convertPerMessageProfileToBeeperFormat,
Expand All @@ -149,6 +150,8 @@
import { PKitProxyMessageHandler } from '$plugins/pluralkit-handler/PKitProxyMessageHandler';
import { SchedulePickerDialog } from './schedule-send';
import * as css from './schedule-send/SchedulePickerDialog.css';
import { PollCreatorDialog } from './poll';
import type { PollCreatorContent } from './poll';
import {
getAudioMsgContent,
getFileMsgContent,
Expand Down Expand Up @@ -364,6 +367,9 @@
);
const [scheduleMenuAnchor, setScheduleMenuAnchor] = useState<RectCords>();
const [showSchedulePicker, setShowSchedulePicker] = useState(false);
const [showPollCreator, setShowPollCreator] = useState(false);
const clientConfig = useClientConfig();
const pollsEnabled = clientConfig.features?.polls ?? false;
const [silentReply, setSilentReply] = useState(!mentionInReplies);
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const isEncrypted = room.hasEncryptionStateEvent();
Expand Down Expand Up @@ -766,6 +772,12 @@
} else if (commandName === Command.UnFlip) {
plainText = `${UNFLIP} ${plainText}`;
customHtml = `${UNFLIP} ${customHtml}`;
} else if (commandName === Command.Poll) {
if (pollsEnabled) setShowPollCreator(true);
resetEditor(editor);
resetEditorHistory(editor);
sendTypingStatus(false);
return;
} else if (commandName) {
const commandContent = commands[commandName as Command];
if (commandContent) {
Expand Down Expand Up @@ -964,6 +976,8 @@
isEncrypted,
setEditingScheduledDelayId,
setScheduledTime,
pollsEnabled,
setShowPollCreator,
]);

const handleKeyDown: KeyboardEventHandler = useCallback(
Expand Down Expand Up @@ -1366,16 +1380,18 @@
</>
}
before={
<IconButton
onClick={() => pickFile('*')}
variant="SurfaceVariant"
size="300"
radii="300"
title="Upload File"
aria-label="Upload and attach a File"
>
<Icon src={Icons.PlusCircle} />
</IconButton>
<Box alignItems="Center" gap="100">
<IconButton
onClick={() => pickFile('*')}
variant="SurfaceVariant"
size="300"
radii="300"
title="Upload File"
aria-label="Upload and attach a File"
>
<Icon src={Icons.PlusCircle} />
</IconButton>
</Box>
}
after={
<>
Expand Down Expand Up @@ -1634,6 +1650,37 @@
}}
/>
)}
{showPollCreator && (
<PollCreatorDialog
onCancel={() => setShowPollCreator(false)}
onSubmit={(content: PollCreatorContent) => {
setShowPollCreator(false);
const pollKindKey = content.kind;
const eventContent: Record<string, unknown> = {
'org.matrix.msc1767.text': content.question,
'org.matrix.msc3381.poll.start': {
question: {
'org.matrix.msc1767.text': content.question,
},
kind: pollKindKey,
max_selections: content.maxSelections,
answers: content.answers.map((a) => ({
id: a.id,
'org.matrix.msc1767.text': a.text,
})),
show_voter_names: content.showVoterNames,
...(content.closesAt !== undefined ? { closes_at: content.closesAt } : {}),
},
};
(mx as any).sendEvent(roomId, 'org.matrix.msc3381.poll.start', eventContent).catch(
// unstable MSC3381 type
(err: unknown) => {
console.error('Failed to send poll:', err);

Check warning on line 1678 in src/app/features/room/RoomInput.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected console statement
}
);
}}
/>
)}
</div>
);
}
Expand Down
49 changes: 49 additions & 0 deletions src/app/features/room/poll/PollCreatorDialog.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { style } from '@vanilla-extract/css';
import { color, config, toRem } from 'folds';

export const DialogContent = style({
padding: config.space.S400,
minWidth: toRem(340),
maxWidth: toRem(500),
display: 'flex',
flexDirection: 'column',
gap: config.space.S300,
maxHeight: `min(80vh, ${toRem(600)})`,
overflowY: 'auto',
});

export const AnswerRow = style({
display: 'flex',
alignItems: 'center',
gap: config.space.S200,
});

export const AnswerInput = style({
flex: 1,
});

export const KindSelector = style({
display: 'flex',
gap: config.space.S200,
});

export const ExpirySelector = style({
display: 'flex',
flexWrap: 'wrap',
gap: config.space.S100,
});

export const DatetimeInput = style({
padding: `${config.space.S100} ${config.space.S200}`,
borderRadius: config.radii.R300,
border: `1px solid ${color.SurfaceVariant.ContainerLine}`,
background: color.SurfaceVariant.Container,
color: 'inherit',
fontSize: config.fontSize.T300,
outline: 'none',
selectors: {
'&:focus': {
borderColor: color.Primary.Main,
},
},
});
Loading
Loading