Add safe search filter for NSFW rooms (#208)

Fix https://github.com/matrix-org/matrix-public-archive/issues/89
This commit is contained in:
Eric Eastwood 2023-05-03 04:45:33 -05:00 committed by GitHub
parent 858c9dde8b
commit b70439e95b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 547 additions and 141 deletions

View File

@ -237,24 +237,22 @@
color: #abb5be;
}
.RoomDirectoryView_roomList {
display: grid;
gap: 20px;
.RoomDirectoryView_mainContentSection {
width: 100%;
max-width: 1180px;
padding-left: 20px;
padding-right: 20px;
margin-top: 20px;
margin-top: 15px;
margin-bottom: 0;
}
.RoomDirectoryView_roomList {
display: grid;
gap: 20px;
}
.RoomDirectoryView_roomListError {
display: block;
width: 100%;
max-width: 1180px;
padding-left: 20px;
padding-right: 20px;
margin-top: 20px;
line-height: 1.5;
}
@ -269,17 +267,14 @@
}
@media (min-width: 750px) {
.RoomDirectoryView_roomList {
grid-template-columns: repeat(2, 1fr);
margin-top: 40px;
.RoomDirectoryView_mainContentSection {
margin-top: 30px;
padding-left: 40px;
padding-right: 40px;
}
.RoomDirectoryView_roomListError {
margin-top: 40px;
padding-left: 40px;
padding-right: 40px;
.RoomDirectoryView_roomList {
grid-template-columns: repeat(2, 1fr);
}
}
@ -289,6 +284,40 @@
}
}
.RoomDirectoryView_safeSearchToggleSection {
display: flex;
justify-content: flex-end;
}
.RoomDirectoryView_safeSearchToggle {
display: flex;
align-items: center;
}
.RoomDirectoryView_safeSearchToggleLabel {
padding-left: 0.3em;
}
.RoomDirectoryView_safeSearchToggleCheckbox {
appearance: none;
position: relative;
width: 20px;
height: 10px;
background-color: #ffffff;
border: 2px solid rgba(200, 200, 200, 1);
box-shadow: inset -10px 0px 0px 0px rgba(200, 200, 200, 1);
border-radius: 10px;
transition: all 0.2s ease;
}
.RoomDirectoryView_safeSearchToggleCheckbox:checked {
border: 2px solid #2774c2;
box-shadow: inset 10px 0px 0px 0px #2774c2;
}
.RoomCardView {
overflow: hidden;
display: flex;
@ -301,6 +330,10 @@
border-radius: 8px;
}
.RoomCardView.blockedBySafeSearch {
background-color: #d0d9e1;
}
.RoomCardView_header {
display: flex;
align-items: top;
@ -350,6 +383,14 @@
text-overflow: ellipsis;
}
.RoomCardView_blockedBySafeSearchTopic {
margin-top: 8px;
margin-bottom: 8px;
line-height: 1.2em;
font-style: italic;
}
.RoomCardView_footer {
display: flex;
/**
@ -398,14 +439,22 @@
cursor: pointer;
}
.RoomCardView_viewButton[disabled] {
border-color: #2774c299;
color: #2774c299;
cursor: auto;
}
@media (max-width: 750px) {
.RoomCardView_viewButton {
padding: 8px 32px;
}
}
.RoomCardView_viewButtonWrapperLink:hover > .RoomCardView_viewButton,
.RoomCardView_viewButtonWrapperLink:focus > .RoomCardView_viewButton {
.RoomCardView_viewButtonWrapperLink:hover > .RoomCardView_viewButton:not([disabled]),
.RoomCardView_viewButtonWrapperLink:focus > .RoomCardView_viewButton:not([disabled]) {
background-color: #2774c2;
color: #ffffff;
}

14
package-lock.json generated
View File

@ -23,7 +23,7 @@
"dompurify": "^2.3.9",
"escape-string-regexp": "^4.0.0",
"express": "^4.17.2",
"hydrogen-view-sdk": "npm:@mlm/hydrogen-view-sdk@^0.26.0-scratch",
"hydrogen-view-sdk": "npm:@mlm/hydrogen-view-sdk@^0.27.0-scratch",
"json5": "^2.2.1",
"linkedom": "^0.14.17",
"matrix-public-archive-shared": "file:./shared/",
@ -3281,9 +3281,9 @@
},
"node_modules/hydrogen-view-sdk": {
"name": "@mlm/hydrogen-view-sdk",
"version": "0.26.0-scratch",
"resolved": "https://registry.npmjs.org/@mlm/hydrogen-view-sdk/-/hydrogen-view-sdk-0.26.0-scratch.tgz",
"integrity": "sha512-rgFCbaI7P5eeaxsTYzfDZ1vII2Sb6uVgK7kut3rqAMPrqnc3Y7AVuY/TUrsBTaviIHJ/OH9suAHISkx9kE29ZA==",
"version": "0.27.0-scratch",
"resolved": "https://registry.npmjs.org/@mlm/hydrogen-view-sdk/-/hydrogen-view-sdk-0.27.0-scratch.tgz",
"integrity": "sha512-wv6GLeAFpdQdAVDSTSiWehslGKxn5DDcS7MtxiL8E2J9OJJkd2VoVncmNrrJ86FN5nORTpbD879HDZ1yevfi9g==",
"dependencies": {
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
"another-json": "^0.2.0",
@ -7522,9 +7522,9 @@
}
},
"hydrogen-view-sdk": {
"version": "npm:@mlm/hydrogen-view-sdk@0.26.0-scratch",
"resolved": "https://registry.npmjs.org/@mlm/hydrogen-view-sdk/-/hydrogen-view-sdk-0.26.0-scratch.tgz",
"integrity": "sha512-rgFCbaI7P5eeaxsTYzfDZ1vII2Sb6uVgK7kut3rqAMPrqnc3Y7AVuY/TUrsBTaviIHJ/OH9suAHISkx9kE29ZA==",
"version": "npm:@mlm/hydrogen-view-sdk@0.27.0-scratch",
"resolved": "https://registry.npmjs.org/@mlm/hydrogen-view-sdk/-/hydrogen-view-sdk-0.27.0-scratch.tgz",
"integrity": "sha512-wv6GLeAFpdQdAVDSTSiWehslGKxn5DDcS7MtxiL8E2J9OJJkd2VoVncmNrrJ86FN5nORTpbD879HDZ1yevfi9g==",
"requires": {
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
"another-json": "^0.2.0",

View File

@ -50,7 +50,7 @@
"dompurify": "^2.3.9",
"escape-string-regexp": "^4.0.0",
"express": "^4.17.2",
"hydrogen-view-sdk": "npm:@mlm/hydrogen-view-sdk@^0.26.0-scratch",
"hydrogen-view-sdk": "npm:@mlm/hydrogen-view-sdk@^0.27.0-scratch",
"json5": "^2.2.1",
"linkedom": "^0.14.17",
"matrix-public-archive-shared": "file:./shared/",

View File

@ -0,0 +1,29 @@
'use strict';
const assert = require('./assert');
const LOCAL_STORAGE_KEYS = {
addedHomeservers: 'addedHomeservers',
safeSearchEnabled: 'safeSearchEnabled',
debugActiveDateIntersectionObserver: 'debugActiveDateIntersectionObserver',
};
// Just make sure they match for sanity. All we really care about is that they are
// unique amongst each other.
Object.keys(LOCAL_STORAGE_KEYS).every((key) => {
const value = LOCAL_STORAGE_KEYS[key];
const doesKeyMatchValue = key === value;
assert(
doesKeyMatchValue,
`LOCAL_STORAGE_KEYS should have keys that are the same as their values for sanity but saw ${key}=${value}.`
);
});
// Make sure all of the keys/values are unique
assert(
new Set(Object.values(LOCAL_STORAGE_KEYS)).length !== Object.values(LOCAL_STORAGE_KEYS).length,
'Duplicate values in LOCAL_STORAGE_KEYS. They should be unique otherwise ' +
'there will be collisions and LocalStorage will be overwritten.'
);
module.exports = LOCAL_STORAGE_KEYS;

View File

@ -85,6 +85,7 @@ async function mountHydrogen() {
urlRouter: urlRouter,
history: archiveHistory,
// Our options
basePath: config.basePath,
homeserverUrl: config.matrixServerUrl,
homeserverName: config.matrixServerName,
matrixPublicArchiveURLCreator,

View File

@ -11,7 +11,7 @@ class AvatarViewModel extends ViewModel {
const { homeserverUrlToPullMediaFrom, avatarUrl, avatarTitle, avatarLetterString, entityId } =
options;
assert(homeserverUrlToPullMediaFrom);
assert(!avatarUrl || homeserverUrlToPullMediaFrom);
assert(avatarTitle);
assert(avatarLetterString);
assert(entityId);

View File

@ -2,8 +2,7 @@
const { ViewModel } = require('hydrogen-view-sdk');
const DEBUG_ACTIVE_DATE_INTERSECTION_OBSERVER_LOCAL_STORAGE_KEY =
'debugActiveDateIntersectionObserver';
const LOCAL_STORAGE_KEYS = require('matrix-public-archive-shared/lib/local-storage-keys');
class DeveloperOptionsContentViewModel extends ViewModel {
constructor(options) {
@ -17,11 +16,13 @@ class DeveloperOptionsContentViewModel extends ViewModel {
loadValuesFromPersistence() {
if (window.localStorage) {
this._debugActiveDateIntersectionObserver = JSON.parse(
window.localStorage.getItem(DEBUG_ACTIVE_DATE_INTERSECTION_OBSERVER_LOCAL_STORAGE_KEY)
window.localStorage.getItem(LOCAL_STORAGE_KEYS.debugActiveDateIntersectionObserver)
);
this.emitChange('debugActiveDateIntersectionObserver');
} else {
console.warn(`Skipping read from LocalStorage since LocalStorage not available`);
console.warn(
`Skipping \`${LOCAL_STORAGE_KEYS.debugActiveDateIntersectionObserver}\` read from LocalStorage since LocalStorage is not available`
);
}
}
@ -32,7 +33,7 @@ class DeveloperOptionsContentViewModel extends ViewModel {
toggleDebugActiveDateIntersectionObserver(checkedValue) {
this._debugActiveDateIntersectionObserver = checkedValue;
window.localStorage.setItem(
DEBUG_ACTIVE_DATE_INTERSECTION_OBSERVER_LOCAL_STORAGE_KEY,
LOCAL_STORAGE_KEYS.debugActiveDateIntersectionObserver,
this._debugActiveDateIntersectionObserver
);
this.emitChange('debugActiveDateIntersectionObserver');

View File

@ -0,0 +1,82 @@
'use strict';
const { ViewModel } = require('hydrogen-view-sdk');
const assert = require('matrix-public-archive-shared/lib/assert');
const MatrixPublicArchiveURLCreator = require('matrix-public-archive-shared/lib/url-creator');
class RoomCardViewModel extends ViewModel {
constructor(options) {
super(options);
const { room, basePath, homeserverUrlToPullMediaFrom, viaServers } = options;
assert(room);
assert(basePath);
assert(homeserverUrlToPullMediaFrom);
assert(viaServers);
this._matrixPublicArchiveURLCreator = new MatrixPublicArchiveURLCreator(basePath);
this._roomId = room.room_id;
this._canonicalAlias = room.canonical_alias;
this._name = room.name;
this._mxcAvatarUrl = room.avatar_url;
this._homeserverUrlToPullMediaFrom = homeserverUrlToPullMediaFrom;
this._numJoinedMembers = room.num_joined_members;
this._topic = room.topic;
this._viaServers = viaServers;
this._blockedBySafeSearch = false;
}
get roomId() {
return this._roomId;
}
get canonicalAlias() {
return this._canonicalAlias;
}
get name() {
return this._name;
}
get mxcAvatarUrl() {
return this._mxcAvatarUrl;
}
get homeserverUrlToPullMediaFrom() {
return this._homeserverUrlToPullMediaFrom;
}
get numJoinedMembers() {
return this._numJoinedMembers;
}
get topic() {
return this._topic;
}
get archiveRoomUrl() {
return this._matrixPublicArchiveURLCreator.archiveUrlForRoom(
this._canonicalAlias || this._roomId,
{
// Only include via servers when we have to fallback to the room ID
viaServers: this._canonicalAlias ? undefined : this._viaServers,
}
);
}
get blockedBySafeSearch() {
return this._blockedBySafeSearch;
}
setBlockedBySafeSearch(blockedBySafeSearch) {
if (blockedBySafeSearch !== this._blockedBySafeSearch) {
this._blockedBySafeSearch = blockedBySafeSearch;
this.emitChange('blockedBySafeSearch');
}
}
}
module.exports = RoomCardViewModel;

View File

@ -1,20 +1,24 @@
'use strict';
const { ViewModel, ObservableArray } = require('hydrogen-view-sdk');
const { ViewModel, ObservableMap, ApplyMap } = require('hydrogen-view-sdk');
const assert = require('matrix-public-archive-shared/lib/assert');
const LOCAL_STORAGE_KEYS = require('matrix-public-archive-shared/lib/local-storage-keys');
const ModalViewModel = require('matrix-public-archive-shared/viewmodels/ModalViewModel');
const HomeserverSelectionModalContentViewModel = require('matrix-public-archive-shared/viewmodels/HomeserverSelectionModalContentViewModel');
const RoomCardViewModel = require('matrix-public-archive-shared/viewmodels/RoomCardViewModel');
const DEFAULT_SERVER_LIST = ['matrix.org', 'gitter.im', 'libera.chat'];
const ADDED_HOMESERVERS_LIST_LOCAL_STORAGE_KEY = 'addedHomeservers';
const NSFW_WORDS = ['nsfw', 'porn', 'nudes', 'sex', '18+'];
const NSFW_REGEXES = NSFW_WORDS.map((word) => new RegExp(`\\b${word}\\b`, 'i'));
class RoomDirectoryViewModel extends ViewModel {
constructor(options) {
super(options);
const {
basePath,
homeserverUrl,
homeserverName,
matrixPublicArchiveURLCreator,
@ -24,6 +28,7 @@ class RoomDirectoryViewModel extends ViewModel {
nextPaginationToken,
prevPaginationToken,
} = options;
assert(basePath);
assert(homeserverUrl);
assert(homeserverName);
assert(matrixPublicArchiveURLCreator);
@ -66,26 +71,32 @@ class RoomDirectoryViewModel extends ViewModel {
})
);
this._rooms = new ObservableArray(
rooms.map((room) => {
return {
roomId: room.room_id,
canonicalAlias: room.canonical_alias,
name: room.name,
mxcAvatarUrl: room.avatar_url,
// This is based off of
// https://github.com/vector-im/hydrogen-web/blob/e77727ea5992a3ec2649edd53a42e6d75f50a0ca/src/domain/session/leftpanel/LeftPanelViewModel.js#L30-L32
this._roomCardViewModelsMap = new ObservableMap();
rooms.forEach((room) => {
this._roomCardViewModelsMap.set(
room.room_id,
new RoomCardViewModel({
room,
basePath,
homeserverUrlToPullMediaFrom: homeserverUrl,
numJoinedMembers: room.num_joined_members,
topic: room.topic,
archiveRoomUrl: matrixPublicArchiveURLCreator.archiveUrlForRoom(
room.canonical_alias || room.room_id,
{
// Only include via servers when we have to fallback to the room ID
viaServers: room.canonical_alias ? undefined : [this.pageSearchParameters.homeserver],
}
),
};
viaServers: [
// If the room is being shown in the directory from this server, then surely
// we can join via this server
this._pageSearchParameters.homeserver,
],
})
);
});
this._roomCardViewModelsFilterMap = new ApplyMap(this._roomCardViewModelsMap);
this._roomCardViewModels = this._roomCardViewModelsFilterMap.sortValues((/*a, b*/) => {
// Sort doesn't matter
return 1;
});
this._safeSearchEnabled = true;
this.loadSafeSearchEnabledFromPersistence();
this.#setupNavigation();
}
@ -171,11 +182,11 @@ class RoomDirectoryViewModel extends ViewModel {
let addedHomeserversFromPersistence = [];
try {
addedHomeserversFromPersistence = JSON.parse(
window.localStorage.getItem(ADDED_HOMESERVERS_LIST_LOCAL_STORAGE_KEY)
window.localStorage.getItem(LOCAL_STORAGE_KEYS.addedHomeservers)
);
} catch (err) {
console.warn(
`Resetting \`${ADDED_HOMESERVERS_LIST_LOCAL_STORAGE_KEY}\` stored in LocalStorage since we ran into an error parsing what was stored`,
`Resetting \`${LOCAL_STORAGE_KEYS.addedHomeservers}\` stored in LocalStorage since we ran into an error parsing what was stored`,
err
);
this.setAddedHomeserversList([]);
@ -184,7 +195,7 @@ class RoomDirectoryViewModel extends ViewModel {
if (!Array.isArray(addedHomeserversFromPersistence)) {
console.warn(
`Resetting \`${ADDED_HOMESERVERS_LIST_LOCAL_STORAGE_KEY}\` stored in LocalStorage since it wasn't an array as expected, addedHomeservers=${addedHomeserversFromPersistence}`
`Resetting \`${LOCAL_STORAGE_KEYS.addedHomeservers}\` stored in LocalStorage since it wasn't an array as expected, addedHomeservers=${addedHomeserversFromPersistence}`
);
this.setAddedHomeserversList([]);
return;
@ -194,7 +205,7 @@ class RoomDirectoryViewModel extends ViewModel {
return;
} else {
console.warn(
`Skipping \`${ADDED_HOMESERVERS_LIST_LOCAL_STORAGE_KEY}\` read from LocalStorage since LocalStorage is not available`
`Skipping \`${LOCAL_STORAGE_KEYS.addedHomeservers}\` read from LocalStorage since LocalStorage is not available`
);
}
}
@ -202,11 +213,11 @@ class RoomDirectoryViewModel extends ViewModel {
setAddedHomeserversList(addedHomeserversList) {
this._addedHomeserversList = addedHomeserversList;
window.localStorage.setItem(
ADDED_HOMESERVERS_LIST_LOCAL_STORAGE_KEY,
LOCAL_STORAGE_KEYS.addedHomeservers,
JSON.stringify(this._addedHomeserversList)
);
// If the added homeserver list changes, make sure the default page selected
// If the added homeserver list changes, make sure the default page-selected
// homeserver is still somewhere in the list. If it's no longer in the added
// homeserver list, we will put it in the default available list.
this._calculateAvailableHomeserverList();
@ -218,6 +229,63 @@ class RoomDirectoryViewModel extends ViewModel {
return this._addedHomeserversList;
}
loadSafeSearchEnabledFromPersistence() {
// Safe search is enabled by default and only disabled with the correct 'false' value
let safeSearchEnabled = true;
if (window.localStorage) {
const safeSearchEnabledFromPersistence = window.localStorage.getItem(
LOCAL_STORAGE_KEYS.safeSearchEnabled
);
if (safeSearchEnabledFromPersistence === 'false') {
safeSearchEnabled = false;
}
} else {
console.warn(
`Skipping \`${LOCAL_STORAGE_KEYS.safeSearchEnabled}\` read from LocalStorage since LocalStorage is not available`
);
}
this.setSafeSearchEnabled(safeSearchEnabled);
}
setSafeSearchEnabled(safeSearchEnabled) {
this._safeSearchEnabled = safeSearchEnabled;
if (window.localStorage) {
window.localStorage.setItem(
LOCAL_STORAGE_KEYS.safeSearchEnabled,
safeSearchEnabled ? 'true' : 'false'
);
} else {
console.warn(
`Skipping \`${LOCAL_STORAGE_KEYS.safeSearchEnabled}\` write to LocalStorage since LocalStorage is not available`
);
}
if (safeSearchEnabled) {
this._roomCardViewModelsFilterMap.setApply((roomId, vm) => {
// We concat the name, topic, etc together to simply do a single check against
// all of the text.
const isNsfw = NSFW_REGEXES.some((regex) =>
regex.test(vm.name + ' ---- ' + vm.canonicalAlias + ' --- ' + vm.topic)
);
vm.setBlockedBySafeSearch(isNsfw);
});
} else {
this._roomCardViewModelsFilterMap.setApply(null);
this._roomCardViewModelsFilterMap.applyOnce((roomId, vm) => {
vm.setBlockedBySafeSearch(false);
});
}
this.emitChange('safeSearchEnabled');
}
get safeSearchEnabled() {
return this._safeSearchEnabled;
}
onNewHomeserverAdded(newHomeserver) {
const addedHomeserversList = this.addedHomeserversList;
this.setAddedHomeserversList(addedHomeserversList.concat(newHomeserver));
@ -284,8 +352,8 @@ class RoomDirectoryViewModel extends ViewModel {
return this._availableHomeserverList;
}
get rooms() {
return this._rooms;
get roomCardViewModels() {
return this._roomCardViewModels;
}
}

View File

@ -1,8 +1,18 @@
'use strict';
const { TemplateView, AvatarView } = require('hydrogen-view-sdk');
const { TemplateView, AvatarView, text } = require('hydrogen-view-sdk');
const AvatarViewModel = require('../viewmodels/AvatarViewModel');
const safeSearchBlockedRoomTitle = 'Blocked by safe search';
const safeSearchBlockedRoomDescription =
'This room was blocked because safe search is turned on and may contain explicit content. Turn off safe search to view this room.';
const blockedBySafeSearchAvatarViewModel = new AvatarViewModel({
avatarTitle: 'x',
avatarLetterString: 'x',
entityId: 'x',
});
class RoomCardView extends TemplateView {
render(t, vm) {
const avatarViewModel = new AvatarViewModel({
@ -31,6 +41,7 @@ class RoomCardView extends TemplateView {
{
className: {
RoomCardView: true,
blockedBySafeSearch: (vm) => vm.blockedBySafeSearch,
},
'data-room-id': vm.roomId,
'data-testid': 'room-card',
@ -39,13 +50,29 @@ class RoomCardView extends TemplateView {
t.a(
{
className: 'RoomCardView_header',
href: vm.archiveRoomUrl,
href: (vm) => {
if (vm.blockedBySafeSearch) {
// Omit the href so the link is not clickable when it's blocked by
// safe search
return false;
}
return vm.archiveRoomUrl;
},
// Since this is the same button as the "View" link, just tab to
// that instead
tabindex: -1,
},
[
t.view(new AvatarView(avatarViewModel, 24)),
t.mapView(
(vm) => vm.blockedBySafeSearch,
(blockedBySafeSearch) => {
if (blockedBySafeSearch) {
return new AvatarView(blockedBySafeSearchAvatarViewModel, 24);
}
return new AvatarView(avatarViewModel, 24);
}
),
t.if(
(vm) => vm.name,
(t /*, vm*/) =>
@ -53,13 +80,30 @@ class RoomCardView extends TemplateView {
{
className: 'RoomCardView_headerTitle',
// We add a title so a tooltip shows the full name on hover
title: displayName,
title: (vm) => {
if (vm.blockedBySafeSearch) {
return safeSearchBlockedRoomTitle;
}
return displayName;
},
displayName
},
[
(vm) => {
if (vm.blockedBySafeSearch) {
return safeSearchBlockedRoomTitle;
}
return displayName;
},
]
)
),
]
),
t.if(
(vm) => !vm.blockedBySafeSearch,
(t /*, vm*/) =>
t.a(
{
className: 'RoomCardView_alias',
@ -69,20 +113,52 @@ class RoomCardView extends TemplateView {
tabindex: -1,
},
[aliasOrRoomId]
)
),
t.if(
(vm) => vm.blockedBySafeSearch,
(t /*, vm*/) =>
t.p({ className: 'RoomCardView_blockedBySafeSearchTopic' }, [
safeSearchBlockedRoomDescription,
])
),
t.if(
(vm) => !vm.blockedBySafeSearch,
(t /*, vm*/) =>
t.p({ className: 'RoomCardView_topic', title: vm.topic || null }, [vm.topic || ''])
),
t.p({ className: 'RoomCardView_topic', title: vm.topic || null }, [vm.topic || '']),
t.div({ className: 'RoomCardView_footer' }, [
t.div({ className: 'RoomCardView_footerInner' }, [
t.div({}, [memberDisplay]),
t.div({}, [
t.if(
(vm) => !vm.blockedBySafeSearch,
(/*t , vm*/) => text(memberDisplay)
),
]),
t.a(
{
className: 'RoomCardView_viewButtonWrapperLink',
href: vm.archiveRoomUrl,
title: `View the ${displayName} room`,
href: (vm) => {
if (vm.blockedBySafeSearch) {
// Omit the href so the link is not clickable when it's blocked by
// safe search
return false;
}
return vm.archiveRoomUrl;
},
title: (vm) => {
if (vm.blockedBySafeSearch) {
return `Turn off safe search to view this room`;
}
return `View the ${displayName} room`;
},
},
t.span(
{
className: 'RoomCardView_viewButton',
disabled: (vm) => vm.blockedBySafeSearch,
},
['View']
)

View File

@ -19,8 +19,8 @@ class RoomDirectoryView extends TemplateView {
const roomList = new ListView(
{
className: 'RoomDirectoryView_roomList',
list: vm.rooms,
className: 'RoomDirectoryView_roomList RoomDirectoryView_mainContentSection',
list: vm.roomCardViewModels,
parentProvidesUpdates: false,
},
(room) => {
@ -210,7 +210,11 @@ class RoomDirectoryView extends TemplateView {
t.if(
(vm) => vm.roomFetchError,
(t, vm) => {
return t.section({ className: 'RoomDirectoryView_roomListError' }, [
return t.section(
{
className: 'RoomDirectoryView_roomListError RoomDirectoryView_mainContentSection',
},
[
t.h3('❗ Unable to fetch rooms from room directory'),
t.p({}, [
`This may be a temporary problem with the homeserver where the room directory lives (${vm.pageSearchParameters.homeserver}) or the homeserver that the archive is pulling from (${vm.homeserverName}). You can try adjusting your search or select a different homeserver to look at. If this problem persists, please check the homeserver status and with a homeserver admin first, then open a `,
@ -275,10 +279,46 @@ class RoomDirectoryView extends TemplateView {
`.`,
]),
]),
]);
]
);
}
),
// Otherwise, display the rooms that we fetched
t.section(
{
className:
'RoomDirectoryView_safeSearchToggleSection RoomDirectoryView_mainContentSection',
},
[
t.div({ className: 'RoomDirectoryView_safeSearchToggle' }, [
t.input({
id: 'safeSearchEnabled',
className: 'RoomDirectoryView_safeSearchToggleCheckbox',
type: 'checkbox',
checked: (vm) => vm.safeSearchEnabled,
onInput: (event) => vm.setSafeSearchEnabled(event.target.checked),
}),
t.label(
{
className: 'RoomDirectoryView_safeSearchToggleLabel',
for: 'safeSearchEnabled',
},
[
t.map(
(vm) => vm.safeSearchEnabled,
(safeSearchEnabled /*, t, vm*/) => {
if (safeSearchEnabled) {
return text('Safe search is on');
}
return text('Safe search is off');
}
),
]
),
]),
]
),
t.view(roomList),
t.div({ className: 'RoomDirectoryView_paginationButtonCombo' }, [
t.a(

View File

@ -4,7 +4,11 @@
# Currently this is based on Complement Synapse images which are based on the
# published 'synapse:latest' image -- ie, the most recent Synapse release.
ARG SYNAPSE_VERSION=latest
# FIXME: We're pinning the version to `v1.79.0` until
# https://github.com/matrix-org/synapse/issues/15526 is fixed. Feel free to update back
# to `latest` once that issue is resolved. More context:
# https://github.com/matrix-org/matrix-public-archive/pull/208#discussion_r1183294630
ARG SYNAPSE_VERSION=v1.79.0
FROM matrixdotorg/synapse:${SYNAPSE_VERSION}

View File

@ -2452,6 +2452,62 @@ describe('matrix-public-archive', () => {
// Assert that the rooms we searched for on remote hs2 are visible
assert.deepStrictEqual(roomsOnPageWithSearch.sort(), [roomXId, roomYId].sort());
});
it('Safe search blocks nsfw rooms by default', async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
// This is just an extra room to fill out the room directory and make sure
// that it does not appear when searching.
await createTestRoom(client);
// Then create some NSFW rooms we will find with search
//
// We use a `timeToken` so that we can namespace these rooms away from other
// test runs against the same homeserver
const timeToken = Date.now();
const roomPlanetPrefix = `planet-${timeToken}`;
const roomUranusId = await createTestRoom(client, {
// NSFW in title
name: `${roomPlanetPrefix}-uranus-nsfw`,
});
const roomMarsId = await createTestRoom(client, {
name: `${roomPlanetPrefix}-mars`,
// NSFW in room topic/description
topic: 'Get your ass to mars (NSFW)',
});
// Browse the room directory searching the room directory for those NSFW rooms
// (narrowing down results).
archiveUrl = matrixPublicArchiveURLCreator.roomDirectoryUrl({
searchTerm: roomPlanetPrefix,
});
const { data: roomDirectoryWithSearchPageHtml } = await fetchEndpointAsText(archiveUrl);
const domWithSearch = parseHTML(roomDirectoryWithSearchPageHtml);
const roomsCardsOnPageWithSearch = [
...domWithSearch.document.querySelectorAll(`[data-testid="room-card"]`),
];
// Assert that the rooms we searched for are on the page
const roomsIdsOnPageWithSearch = roomsCardsOnPageWithSearch.map((roomCardEl) => {
return roomCardEl.getAttribute('data-room-id');
});
assert.deepStrictEqual(roomsIdsOnPageWithSearch.sort(), [roomUranusId, roomMarsId].sort());
// Sanity check that safe search does something. Assert that it's *NOT* showing
// the "nsfw" content
roomsCardsOnPageWithSearch.forEach((roomCardEl) => {
assert.match(
roomCardEl.innerHTML,
/^((?!nsfw).)*$/,
`Expected safe search to block any nsfw rooms but saw "nsfw" in the room cards: ${roomCardEl.innerHTML.replaceAll(
/nsfw/gi,
(match) => {
return chalk.yellow(match);
}
)}`
);
});
});
});
describe('access controls', () => {