diff --git a/src/app/features/common-settings/general/RoomProfile.tsx b/src/app/features/common-settings/general/RoomProfile.tsx
index a48a65307..4279e8325 100644
--- a/src/app/features/common-settings/general/RoomProfile.tsx
+++ b/src/app/features/common-settings/general/RoomProfile.tsx
@@ -40,6 +40,8 @@ import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { StateEvent } from '../../../../types/matrix/room';
import { sendStateEvent } from '../../../utils/room';
+import { describeRoomVisibility } from '../../../utils/roomVisibilityLine';
+import { useStateEvent } from '../../../hooks/useStateEvent';
import { CompactUploadCardRenderer } from '../../../components/upload-card';
import { useObjectURL } from '../../../hooks/useObjectURL';
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
@@ -434,6 +436,14 @@ export function RoomProfile({ permissions }: RoomProfileProps) {
const name = useRoomName(room);
const topic = useRoomTopic(room);
const joinRule = useRoomJoinRule(room);
+ // [Gitea #133] One line, no controls: encryption · join rule · history.
+ const historyVisibilityEvent = useStateEvent(room, StateEvent.RoomHistoryVisibility);
+ const visibilityLine = describeRoomVisibility({
+ encrypted: room.hasEncryptionStateEvent(),
+ joinRule: joinRule?.join_rule,
+ historyVisibility: historyVisibilityEvent?.getContent<{ history_visibility?: string }>()
+ .history_visibility,
+ });
const canEditAvatar = permissions.stateEvent(StateEvent.RoomAvatar, mx.getSafeUserId());
const canEditName = permissions.stateEvent(StateEvent.RoomName, mx.getSafeUserId());
@@ -474,6 +484,9 @@ export function RoomProfile({ permissions }: RoomProfileProps) {
{name ?? 'Unknown'}
+
+ {visibilityLine}
+
{topic && (
{topic.format === 'org.matrix.custom.html' &&
diff --git a/src/app/utils/roomVisibilityLine.test.ts b/src/app/utils/roomVisibilityLine.test.ts
new file mode 100644
index 000000000..fef65e505
--- /dev/null
+++ b/src/app/utils/roomVisibilityLine.test.ts
@@ -0,0 +1,27 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { describeRoomVisibility } from './roomVisibilityLine';
+
+test('describeRoomVisibility composes the three facts', () => {
+ assert.equal(
+ describeRoomVisibility({ encrypted: true, joinRule: 'invite', historyVisibility: 'joined' }),
+ 'Encrypted · Invite only · History visible to members since they joined',
+ );
+ assert.equal(
+ describeRoomVisibility({ encrypted: false, joinRule: 'public', historyVisibility: 'shared' }),
+ 'Not encrypted · Anyone can join · History visible to members, including before they joined',
+ );
+ assert.equal(
+ describeRoomVisibility({
+ encrypted: false,
+ joinRule: 'knock',
+ historyVisibility: 'world_readable',
+ }),
+ 'Not encrypted · Ask to join · History visible to anyone, even guests',
+ );
+ // Missing state: spec defaults are invite-only and "shared".
+ assert.equal(
+ describeRoomVisibility({ encrypted: true }),
+ 'Encrypted · Invite only · History visible to members, including before they joined',
+ );
+});
diff --git a/src/app/utils/roomVisibilityLine.ts b/src/app/utils/roomVisibilityLine.ts
new file mode 100644
index 000000000..9fc8fa5dc
--- /dev/null
+++ b/src/app/utils/roomVisibilityLine.ts
@@ -0,0 +1,49 @@
+import { HistoryVisibility, JoinRule } from 'matrix-js-sdk';
+
+/**
+ * [Gitea #133] One sentence for Room Settings → General, under the name:
+ * "Encrypted · Invite only · History visible to members since they joined".
+ * Wording mirrors the room preview card chips and the history-visibility
+ * setting so the same words mean the same thing everywhere.
+ */
+export function describeRoomVisibility(input: {
+ encrypted: boolean;
+ joinRule?: string;
+ historyVisibility?: string;
+}): string {
+ const parts: string[] = [input.encrypted ? 'Encrypted' : 'Not encrypted'];
+ switch (input.joinRule) {
+ case JoinRule.Public:
+ parts.push('Anyone can join');
+ break;
+ case JoinRule.Knock:
+ case 'knock_restricted':
+ parts.push('Ask to join');
+ break;
+ case JoinRule.Restricted:
+ parts.push('Restricted');
+ break;
+ case JoinRule.Private:
+ parts.push('Private');
+ break;
+ case JoinRule.Invite:
+ default:
+ parts.push('Invite only');
+ }
+ switch (input.historyVisibility) {
+ case HistoryVisibility.WorldReadable:
+ parts.push('History visible to anyone, even guests');
+ break;
+ case HistoryVisibility.Invited:
+ parts.push('History visible to members since they were invited');
+ break;
+ case HistoryVisibility.Joined:
+ parts.push('History visible to members since they joined');
+ break;
+ case HistoryVisibility.Shared:
+ default:
+ // "shared" is also the spec default when the state event is absent.
+ parts.push('History visible to members, including before they joined');
+ }
+ return parts.join(' · ');
+}