Add safe search filter for NSFW rooms (#208)
Fix https://github.com/matrix-org/matrix-public-archive/issues/89
This commit is contained in:
parent
858c9dde8b
commit
b70439e95b
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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/",
|
||||
|
|
|
@ -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;
|
|
@ -85,6 +85,7 @@ async function mountHydrogen() {
|
|||
urlRouter: urlRouter,
|
||||
history: archiveHistory,
|
||||
// Our options
|
||||
basePath: config.basePath,
|
||||
homeserverUrl: config.matrixServerUrl,
|
||||
homeserverName: config.matrixServerName,
|
||||
matrixPublicArchiveURLCreator,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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']
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
Loading…
Reference in New Issue