Compare commits

...
Author SHA1 Message Date
Ajay Bura d2a9e179f0 Fix bug
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-21 08:48:36 +05:30
Ajay Bura 321ee96620 Update string
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-20 22:03:55 +05:30
Ajay Bura ea98418e50 Redesign user profile in settings
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-20 21:59:54 +05:30
Ajay Bura 4265b97419 Redesign app settings
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-20 19:10:06 +05:30
Ajay Bura abb81b6390 Remove opacity from jump to unread and latest btn (#402)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-20 09:19:20 +05:30
Ajay Bura 36da3d3cba Add sound on incoming invites (#400)
* Add invite sound

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add credits

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Change invite sound to google material
2022-03-19 18:55:57 +05:30
Ajay Bura ae71a99aa4 Sort search results by activity (#399)
* Sort search result by activity

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Optimize generateResults function codes

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-19 18:55:38 +05:30
Ajay Bura a7034d6351 Improve jump to bottom btn
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-18 11:37:31 +05:30
Ajay Bura 6010b4c252 Fix cyclic dependency in initMatrix
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-18 09:22:16 +05:30
Ajay Bura 8330f4fba9 Improve jump to unread button (#396)
* Improve jump to unread button

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Remove unused cod

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix mark as read not hidding jump to unread btn

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add notification mark as read action

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add esc as hotkey to mark room as read

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add message icons

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Change jump to unread icon

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-18 09:09:14 +05:30
ginnyTheCat dc6e153b92 Notification sounds (#367)
* Basic notification sound support

* Add settings option for notification sounds

* Allow sound without desktop notifications
2022-03-18 09:07:11 +05:30
Nitan Alexandru Marcel a2655ee6a5 Fix loading on older browsers (#397) 2022-03-18 09:06:48 +05:30
Ajay Bura 13248962af Update recent clock icon
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-17 17:35:50 +05:30
ginnyTheCatandKrishan b698982186 Add recently used section to emoji board (#373)
* Add recent section to emoji board

* Add section to emoji board sidebar

* Add emoji limit like element web has

* Ignore custom emojis

* Filter out invalid emojis

* Update heart icon with clock

Co-authored-by: Krishan <33421343+kfiven@users.noreply.github.com>
2022-03-17 17:19:14 +05:30
Ajay Bura 5a299b21c5 Sort direct messages by activity (#393)
* Add sort util

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Use sort util for members

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Sort dms by activity

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Sort dms activily

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Chanege roomIdByLastActive func name

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-17 16:55:16 +05:30
Krishan bb90f11ec8 Formify issue templates and improve existing (#395)
* Update bug_report.md

* Rename bug_report.md to bug_report.yml

* Update and rename feature_request.md to feature_request.yml

* Create config.yml

* Update PULL_REQUEST_TEMPLATE.md
2022-03-17 16:47:42 +05:30
50 changed files with 878 additions and 596 deletions

No files matched your search

-33
View File
@@ -1,33 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
#### Describe the bug
A clear and concise description of what the bug is.
#### To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
#### Expected behavior
A clear and concise description of what you expected to happen.
#### Screenshots
If applicable, add screenshots to help explain your problem.
#### Desktop (please complete the following information):
- OS: [e.g. Windows, MacOS]
- Browser: [e.g. chrome, firefox]
- Version: [e.g. 3.22]
- Matrix homeserver: [e.g. matrix.org]
#### Additional context
Add any other context about the problem here.
+59
View File
@@ -0,0 +1,59 @@
name: 🐞 Bug Report
title: '[bug] '
description: Report a bug
labels: 'type: bug'
body:
- type: markdown
attributes:
value: |
## First of all
1. Please search for [existing issues](https://github.com/ajbura/cinny/issues?q=is%3Aissue) about this problem first.
2. Make sure Cinny is up to date.
3. Make sure it's an issue with Cinny and not something else you are using.
4. Remember to be friendly.
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear description of what the bug is. Include screenshots if applicable.
placeholder: Bug description
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Steps to reproduce the behavior.
placeholder: |
1. Go to ...
2. Click on ...
3. See error
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear description of what you expected to happen.
- type: textarea
id: info
attributes:
label: Platform and versions
description: "Provide OS, browser and Cinny version with your Homeserver."
placeholder: |
1. OS: [e.g. Windows 10, MacOS]
2. Browser: [e.g. chrome 99.5, firefox 97.2]
3. Cinny version: [e.g. 1.8.1 (app.cinny.in)]
4. Matrix homeserver: [e.g. matrix.org]
render: shell
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.
+4
View File
@@ -0,0 +1,4 @@
contact_links:
- name: 💬 Matrix Chat
url: https://matrix.to/#/#cinny:matrix.org
about: Ask questions and talk to other Cinny users and the maintainers
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
#### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
#### Describe the solution you'd like
A clear and concise description of what you want to happen.
#### Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
#### Additional context
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,35 @@
name: 💡 Feature Request
title: '[feat] '
description: Suggest an idea
labels: 'type: feature'
body:
- type: textarea
id: problem
attributes:
label: Describe the problem
description: A clear description of the problem this feature would solve
placeholder: "I'm always frustrated when..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: "Describe the solution you'd like"
description: A clear description of what change you would like
placeholder: "I would like to..."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: "Any alternative solutions you've considered"
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.
+2 -4
View File
@@ -1,15 +1,13 @@
<!-- Please read https://github.com/ajbura/cinny/CONTRIBUTING.md before submitting your pull request -->
### Description
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
Fixes # (issue)
Fixes #
#### Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<rect x="7" y="12" fill="#010101" width="10" height="2"/>
<g>
<circle fill="#010101" cx="19" cy="6" r="4"/>
</g>
<path fill="#010101" d="M13.3,8H7v2h7.5C14,9.4,13.6,8.7,13.3,8z"/>
<path fill="#010101" d="M19,12v5.6l-2.4-1.3L16.1,16h-0.5H5V6h8c0-0.7,0.1-1.4,0.3-2H4.8C3.8,4,3,4.9,3,6v10c0,1.1,0.8,2,1.8,2
h10.8l5.4,3v-9.3C20.4,11.9,19.7,12,19,12z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 811 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<path fill="#010101" d="M19,6v11.6l-2.4-1.3L16.1,16h-0.5H5V6H19 M19.2,4H4.8C3.8,4,3,4.9,3,6v10c0,1.1,0.8,2,1.8,2h10.8l5.4,3V6
C21,4.9,20.2,4,19.2,4L19.2,4z"/>
<rect x="7" y="8" fill="#010101" width="10" height="2"/>
<rect x="7" y="12" fill="#010101" width="10" height="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 732 B

+7 -6
View File
@@ -4,13 +4,14 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<polygon fill="#010101" points="11,6 11,12 15.2,16.2 16.7,14.8 13,11.2 13,6 "/>
<path fill="#010101" d="M12,2C6.5,2,2,6.5,2,12H0.2L3,14.8L5.8,12H4c0-4.4,3.6-8,8-8s8,3.6,8,8s-3.6,8-8,8c-1.9,0-3.7-0.7-5-1.8
l-1.2,1.6C7.4,21.2,9.6,22,12,22c5.5,0,10-4.5,10-10S17.5,2,12,2z"/>
<polygon points="11,7 11,13 14.5,16.5 15.9,15.1 13,12.2 13,7 "/>
<path d="M12,2C8.7,2,5.8,3.6,4,6V4H2.5v5h5V7.5H5.4C6.9,5.4,9.3,4,12,4c4.4,0,8,3.6,8,8s-3.6,8-8,8s-8-3.6-8-8H2
c0,5.5,4.5,10,10,10s10-4.5,10-10S17.5,2,12,2z"/>
</g>
<g>
<polygon fill="#010101" points="49,44 49,50 53.2,54.2 54.7,52.8 51,49.2 51,44 "/>
<path fill="#010101" d="M50,40c-5.5,0-10,4.5-10,10h-1.8l2.8,2.8l2.8-2.8H42c0-4.4,3.6-8,8-8s8,3.6,8,8s-3.6,8-8,8
c-1.9,0-3.7-0.7-5-1.8l-1.2,1.6c1.7,1.4,3.9,2.2,6.3,2.2c5.5,0,10-4.5,10-10S55.5,40,50,40z"/>
<polygon points="49,44 49,50 53.2,54.2 54.7,52.8 51,49.2 51,44 "/>
<polygon points="45.5,47 40.5,47 40.5,42 42,42 42,45.5 45.5,45.5 "/>
<path d="M50,40c-4.1,0-7.6,2.5-9.2,6h2.2c1.4-2.4,4-4,6.9-4c4.4,0,8,3.6,8,8s-3.6,8-8,8s-8-3.6-8-8h-2c0,5.5,4.5,10,10,10
s10-4.5,10-10S55.5,40,50,40z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 985 B

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -74,7 +74,7 @@ Tabs.defaultProps = {
Tabs.propTypes = {
items: PropTypes.arrayOf(
PropTypes.exact({
PropTypes.shape({
iconSrc: PropTypes.string,
text: PropTypes.string,
disabled: PropTypes.bool,
@@ -84,4 +84,4 @@ Tabs.propTypes = {
onSelect: PropTypes.func.isRequired,
};
export { Tabs as default };
export default Tabs;
+2 -21
View File
@@ -9,6 +9,7 @@ import colorMXID from '../../../util/colorMXID';
import { openProfileViewer } from '../../../client/action/navigation';
import { getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
import AsyncSearch from '../../../util/AsyncSearch';
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
@@ -19,26 +20,6 @@ import PeopleSelector from '../people-selector/PeopleSelector';
const PER_PAGE_MEMBER = 50;
function AtoZ(m1, m2) {
const aName = m1.name;
const bName = m2.name;
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
function sortByPowerLevel(m1, m2) {
const pl1 = m1.powerLevel;
const pl2 = m2.powerLevel;
if (pl1 > pl2) return -1;
if (pl1 < pl2) return 1;
return 0;
}
function normalizeMembers(members) {
const mx = initMatrix.matrixClient;
return members.map((member) => ({
@@ -65,7 +46,7 @@ function useMemberOfMembership(roomId, membership) {
if (event && event?.getRoomId() !== roomId) return;
const memberOfMembership = normalizeMembers(
room.getMembersWithMembership(membership)
.sort(AtoZ).sort(sortByPowerLevel),
.sort(memberByAtoZ).sort(memberByPowerLevel),
);
setMembers(memberOfMembership);
};
@@ -6,6 +6,7 @@ import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { openInviteUser } from '../../../client/action/navigation';
import * as roomActions from '../../../client/action/room';
import { markAsRead } from '../../../client/action/notifications';
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
import RoomNotification from '../room-notification/RoomNotification';
@@ -20,10 +21,8 @@ function RoomOptions({ roomId, afterOptionSelect }) {
const canInvite = room?.canInvite(mx.getUserId());
const handleMarkAsRead = () => {
markAsRead(roomId);
afterOptionSelect();
if (!room) return;
const events = room.getLiveTimeline().getEvents();
mx.sendReadReceipt(events[events.length - 1]);
};
const handleInviteClick = () => {
@@ -88,7 +88,7 @@ function SpaceAddExistingContent({ roomId }) {
};
const handleSearch = (ev) => {
const term = ev.target.value.toLocaleLowerCase().replaceAll(' ', '');
const term = ev.target.value.toLocaleLowerCase().replace(/\s/g, '');
if (term === '') {
setSearchIds(null);
return;
@@ -100,7 +100,7 @@ function SpaceAddExistingContent({ roomId }) {
if (!name) return false;
name = name.normalize('NFKC')
.toLocaleLowerCase()
.replaceAll(' ', '');
.replace(/\s/g, '');
return name.includes(term);
});
setSearchIds(searchedIds);
+25 -6
View File
@@ -12,6 +12,7 @@ import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import AsyncSearch from '../../../util/AsyncSearch';
import { addRecentEmoji, getRecentEmojis } from './recent';
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
@@ -20,6 +21,7 @@ import Input from '../../atoms/input/Input';
import ScrollView from '../../atoms/scroll/ScrollView';
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import RecentClockIC from '../../../../public/res/ic/outlined/recent-clock.svg';
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
import DogIC from '../../../../public/res/ic/outlined/dog.svg';
import CupIC from '../../../../public/res/ic/outlined/cup.svg';
@@ -29,10 +31,11 @@ import BulbIC from '../../../../public/res/ic/outlined/bulb.svg';
import PeaceIC from '../../../../public/res/ic/outlined/peace.svg';
import FlagIC from '../../../../public/res/ic/outlined/flag.svg';
const ROW_EMOJIS_COUNT = 7;
const EmojiGroup = React.memo(({ name, groupEmojis }) => {
function getEmojiBoard() {
const emojiBoard = [];
const ROW_EMOJIS_COUNT = 7;
const totalEmojis = groupEmojis.length;
for (let r = 0; r < totalEmojis; r += ROW_EMOJIS_COUNT) {
@@ -147,8 +150,9 @@ function EmojiBoard({ onSelect, searchRef }) {
function selectEmoji(e) {
if (isTargetNotEmoji(e.target)) return;
const emoji = e.target;
onSelect(getEmojiDataFromTarget(emoji));
const emoji = getEmojiDataFromTarget(e.target);
onSelect(emoji);
if (emoji.hexcode) addRecentEmoji(emoji.unicode);
}
function setEmojiInfo(emoji) {
@@ -188,6 +192,9 @@ function EmojiBoard({ onSelect, searchRef }) {
}
const [availableEmojis, setAvailableEmojis] = useState([]);
const [recentEmojis, setRecentEmojis] = useState([]);
const recentOffset = recentEmojis.length > 0 ? 1 : 0;
useEffect(() => {
const updateAvailableEmoji = (selectedRoomId) => {
@@ -215,6 +222,9 @@ function EmojiBoard({ onSelect, searchRef }) {
const onOpen = () => {
searchRef.current.value = '';
handleSearchChange();
// only update when board is getting opened to prevent shifting UI
setRecentEmojis(getRecentEmojis(3 * ROW_EMOJIS_COUNT));
};
navigation.on(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
@@ -230,7 +240,7 @@ function EmojiBoard({ onSelect, searchRef }) {
const $emojiContent = scrollEmojisRef.current.firstElementChild;
const groupCount = $emojiContent.childElementCount;
if (groupCount > emojiGroups.length) {
tabIndex += groupCount - emojiGroups.length - availableEmojis.length;
tabIndex += groupCount - emojiGroups.length - availableEmojis.length - recentOffset;
}
$emojiContent.children[tabIndex].scrollIntoView();
}
@@ -246,6 +256,7 @@ function EmojiBoard({ onSelect, searchRef }) {
<ScrollView ref={scrollEmojisRef} autoHide>
<div onMouseMove={hoverEmoji} onClick={selectEmoji}>
<SearchedEmoji />
{recentEmojis.length > 0 && <EmojiGroup name="Recently used" groupEmojis={recentEmojis} />}
{
availableEmojis.map((pack) => (
<EmojiGroup
@@ -271,13 +282,21 @@ function EmojiBoard({ onSelect, searchRef }) {
</div>
<ScrollView invisible>
<div className="emoji-board__nav">
{recentEmojis.length > 0 && (
<IconButton
onClick={() => openGroup(0)}
src={RecentClockIC}
tooltip="Recent"
tooltipPlacement="right"
/>
)}
<div className="emoji-board__nav-custom">
{
availableEmojis.map((pack) => {
const src = initMatrix.matrixClient.mxcUrlToHttp(pack.avatar ?? pack.images[0].mxc);
return (
<IconButton
onClick={() => openGroup(pack.packIndex)}
onClick={() => openGroup(recentOffset + pack.packIndex)}
src={src}
key={pack.packIndex}
tooltip={pack.displayName}
@@ -301,7 +320,7 @@ function EmojiBoard({ onSelect, searchRef }) {
[7, FlagIC, 'Flags'],
].map(([indx, ico, name]) => (
<IconButton
onClick={() => openGroup(availableEmojis.length + indx)}
onClick={() => openGroup(recentOffset + availableEmojis.length + indx)}
key={indx}
src={ico}
tooltip={name}
+36
View File
@@ -0,0 +1,36 @@
import initMatrix from '../../../client/initMatrix';
import { emojis } from './emoji';
const eventType = 'io.element.recent_emoji';
function getRecentEmojisRaw() {
return initMatrix.matrixClient.getAccountData(eventType).getContent().recent_emoji ?? [];
}
export function getRecentEmojis(limit) {
const res = [];
getRecentEmojisRaw()
.sort((a, b) => b[1] - a[1])
.find(([unicode]) => {
const emoji = emojis.find((e) => e.unicode === unicode);
if (emoji) return res.push(emoji) >= limit;
return false;
});
return res;
}
export function addRecentEmoji(unicode) {
const recent = getRecentEmojisRaw();
const i = recent.findIndex(([u]) => u === unicode);
let entry;
if (i < 0) {
entry = [unicode, 1];
} else {
[entry] = recent.splice(i, 1);
entry[1] += 1;
}
recent.unshift(entry);
initMatrix.matrixClient.setAccountData(eventType, {
recent_emoji: recent.slice(0, 100),
});
}
+24 -4
View File
@@ -1,18 +1,38 @@
import React, { useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import Postie from '../../../util/Postie';
import { roomIdByActivity } from '../../../util/sort';
import RoomsCategory from './RoomsCategory';
import { AtoZ } from './common';
const drawerPostie = new Postie();
function Directs() {
const mx = initMatrix.matrixClient;
const { roomList, notifications } = initMatrix;
const directIds = [...roomList.directs].sort(AtoZ);
const [directIds, setDirectIds] = useState([]);
useEffect(() => setDirectIds([...roomList.directs].sort(roomIdByActivity)), []);
useEffect(() => {
const handleTimeline = (event, room, toStartOfTimeline, removed, data) => {
if (!roomList.directs.has(room.roomId)) return;
if (!data.liveEvent) return;
if (directIds[0] === room.roomId) return;
const newDirectIds = [room.roomId];
directIds.forEach((id) => {
if (id === room.roomId) return;
newDirectIds.push(id);
});
setDirectIds(newDirectIds);
};
mx.on('Room.timeline', handleTimeline);
return () => {
mx.removeListener('Room.timeline', handleTimeline);
};
}, [directIds]);
useEffect(() => {
const selectorChanged = (selectedRoomId, prevSelectedRoomId) => {
+23 -17
View File
@@ -5,11 +5,11 @@ import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import Postie from '../../../util/Postie';
import { roomIdByActivity, roomIdByAtoZ } from '../../../util/sort';
import RoomsCategory from './RoomsCategory';
import { useCategorizedSpaces } from '../../hooks/useCategorizedSpaces';
import { AtoZ, RoomToDM } from './common';
const drawerPostie = new Postie();
function Home({ spaceId }) {
@@ -34,10 +34,6 @@ function Home({ spaceId }) {
roomIds = roomList.getOrphanRooms();
}
spaceIds.sort(AtoZ);
roomIds.sort(AtoZ);
directIds.sort(AtoZ);
if (isCategorized) {
categories = roomList.getCategorizedSpaces(spaceIds);
categories.delete(spaceId);
@@ -73,26 +69,36 @@ function Home({ spaceId }) {
return (
<>
{ !isCategorized && spaceIds.length !== 0 && (
<RoomsCategory name="Spaces" roomIds={spaceIds} drawerPostie={drawerPostie} />
<RoomsCategory name="Spaces" roomIds={spaceIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
)}
{ roomIds.length !== 0 && (
<RoomsCategory name="Rooms" roomIds={roomIds} drawerPostie={drawerPostie} />
<RoomsCategory name="Rooms" roomIds={roomIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
)}
{ directIds.length !== 0 && (
<RoomsCategory name="People" roomIds={directIds} drawerPostie={drawerPostie} />
<RoomsCategory name="People" roomIds={directIds.sort(roomIdByActivity)} drawerPostie={drawerPostie} />
)}
{ isCategorized && [...categories].map(([catId, childIds]) => (
<RoomsCategory
key={catId}
spaceId={catId}
name={mx.getRoom(catId).name}
roomIds={[...childIds].sort(AtoZ).sort(RoomToDM)}
drawerPostie={drawerPostie}
/>
))}
{ isCategorized && [...categories].map(([catId, childIds]) => {
const rms = [];
const dms = [];
childIds.forEach((id) => {
if (directs.has(id)) dms.push(id);
else rms.push(id);
});
rms.sort(roomIdByAtoZ);
dms.sort(roomIdByActivity);
return (
<RoomsCategory
key={catId}
spaceId={catId}
name={mx.getRoom(catId).name}
roomIds={rms.concat(dms)}
drawerPostie={drawerPostie}
/>
);
})}
</>
);
}
+1 -1
View File
@@ -72,7 +72,7 @@ function ProfileAvatarMenu() {
return (
<SidebarAvatar
onClick={openSettings}
tooltip={profile.displayName}
tooltip="Settings"
avatar={(
<Avatar
text={profile.displayName}
-30
View File
@@ -1,30 +0,0 @@
import initMatrix from '../../../client/initMatrix';
function AtoZ(aId, bId) {
let aName = initMatrix.matrixClient.getRoom(aId).name;
let bName = initMatrix.matrixClient.getRoom(bId).name;
// remove "#" from the room name
// To ignore it in sorting
aName = aName.replaceAll('#', '');
bName = bName.replaceAll('#', '');
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
const RoomToDM = (aId, bId) => {
const { directs } = initMatrix.roomList;
const aIsDm = directs.has(aId);
const bIsDm = directs.has(bId);
if (aIsDm && !bIsDm) return 1;
if (!aIsDm && bIsDm) return -1;
return 0;
};
export { AtoZ, RoomToDM };
@@ -1,35 +1,44 @@
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import ImageUpload from '../../molecules/image-upload/ImageUpload';
import Input from '../../atoms/input/Input';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import './ProfileEditor.scss';
// TODO Fix bug that prevents 'Save' button from enabling up until second changed.
function ProfileEditor({
userId,
}) {
function ProfileEditor({ userId }) {
const [isEditing, setIsEditing] = useState(false);
const mx = initMatrix.matrixClient;
const user = mx.getUser(mx.getUserId());
const displayNameRef = useRef(null);
const bgColor = colorMXID(userId);
const [avatarSrc, setAvatarSrc] = useState(null);
const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null);
const [username, setUsername] = useState(user.displayName);
const [disabled, setDisabled] = useState(true);
let username = mx.getUser(mx.getUserId()).displayName;
useEffect(() => {
let isMounted = true;
mx.getProfileInfo(mx.getUserId()).then((info) => {
if (!isMounted) return;
setAvatarSrc(info.avatar_url ? mx.mxcUrlToHttp(info.avatar_url, 80, 80, 'crop') : null);
setUsername(info.displayname);
});
return () => {
isMounted = false;
};
}, [userId]);
// Sets avatar URL and updates the avatar component in profile editor to reflect new upload
function handleAvatarUpload(url) {
const handleAvatarUpload = (url) => {
if (url === null) {
if (confirm('Are you sure you want to remove avatar?')) {
mx.setAvatarUrl('');
@@ -39,48 +48,72 @@ function ProfileEditor({
}
mx.setAvatarUrl(url);
setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop'));
}
};
function saveDisplayName() {
const saveDisplayName = () => {
const newDisplayName = displayNameRef.current.value;
if (newDisplayName !== null && newDisplayName !== username) {
mx.setDisplayName(newDisplayName);
username = newDisplayName;
setUsername(newDisplayName);
setDisabled(true);
setIsEditing(false);
}
}
};
function onDisplayNameInputChange() {
const onDisplayNameInputChange = () => {
setDisabled(username === displayNameRef.current.value || displayNameRef.current.value == null);
}
function cancelDisplayNameChanges() {
};
const cancelDisplayNameChanges = () => {
displayNameRef.current.value = username;
onDisplayNameInputChange();
}
setIsEditing(false);
};
return (
const renderForm = () => (
<form
className="profile-editor"
className="profile-editor__form"
style={{ marginBottom: avatarSrc ? '24px' : '0' }}
onSubmit={(e) => { e.preventDefault(); saveDisplayName(); }}
>
<Input
label={`Display name of ${mx.getUserId()}`}
onChange={onDisplayNameInputChange}
value={mx.getUser(mx.getUserId()).displayName}
forwardRef={displayNameRef}
/>
<Button variant="primary" type="submit" disabled={disabled}>Save</Button>
<Button onClick={cancelDisplayNameChanges}>Cancel</Button>
</form>
);
const renderInfo = () => (
<div className="profile-editor__info" style={{ marginBottom: avatarSrc ? '24px' : '0' }}>
<div>
<Text variant="h2" primary weight="medium">{twemojify(username)}</Text>
<IconButton
src={PencilIC}
size="extra-small"
tooltip="Edit"
onClick={() => setIsEditing(true)}
/>
</div>
<Text variant="b2">{mx.getUserId()}</Text>
</div>
);
return (
<div className="profile-editor">
<ImageUpload
text={username}
bgColor={bgColor}
bgColor={colorMXID(userId)}
imageSrc={avatarSrc}
onUpload={handleAvatarUpload}
onRequestRemove={() => handleAvatarUpload(null)}
/>
<div className="profile-editor__input-wrapper">
<Input
label={`Display name of ${mx.getUserId()}`}
onChange={onDisplayNameInputChange}
value={mx.getUser(mx.getUserId()).displayName}
forwardRef={displayNameRef}
/>
<Button variant="primary" type="submit" disabled={disabled}>Save</Button>
<Button onClick={cancelDisplayNameChanges}>Cancel</Button>
</div>
</form>
{
isEditing ? renderForm() : renderInfo()
}
</div>
);
}
@@ -1,28 +1,41 @@
@use '../../partials/dir';
@use '../../partials/flex';
.profile-editor {
display: flex;
align-items: flex-start;
align-items: flex-end;
}
.profile-editor__input-wrapper {
flex: 1;
min-width: 0;
margin-top: 10px;
.profile-editor__info,
.profile-editor__form {
@extend .cp-fx__item-one;
@include dir.side(margin, var(--sp-loose), 0);
display: flex;
align-items: flex-end;
}
.profile-editor__info {
flex-direction: column;
& > div:first-child {
display: flex;
align-items: center;
}
.ic-btn {
margin: 0 var(--sp-extra-tight);
}
}
.profile-editor__form {
margin-top: 10px;
flex-wrap: wrap;
align-items: flex-end;
& > .input-container {
flex: 1;
@extend .cp-fx__item-one;
}
& > button {
height: 46px;
margin-top: var(--sp-normal);
}
& > * {
@include dir.side(margin, var(--sp-normal), 0);
}
}
+1 -10
View File
@@ -18,7 +18,6 @@ function Windows() {
const [inviteUser, changeInviteUser] = useState({
isOpen: false, roomId: undefined, term: undefined,
});
const [settings, changeSettings] = useState(false);
function openInviteList() {
changeInviteList(true);
@@ -36,20 +35,15 @@ function Windows() {
searchTerm,
});
}
function openSettings() {
changeSettings(true);
}
useEffect(() => {
navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
return () => {
navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings);
};
}, []);
@@ -70,10 +64,7 @@ function Windows() {
searchTerm={inviteUser.searchTerm}
onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })}
/>
<Settings
isOpen={settings}
onRequestClose={() => changeSettings(false)}
/>
<Settings />
<SpaceSettings />
<SpaceManage />
</>
+2 -21
View File
@@ -9,6 +9,7 @@ import { getPowerLabel, getUsernameOfRoomMember } from '../../../util/matrixUtil
import colorMXID from '../../../util/colorMXID';
import { openInviteUser, openProfileViewer } from '../../../client/action/navigation';
import AsyncSearch from '../../../util/AsyncSearch';
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
import Text from '../../atoms/text/Text';
import Header, { TitleWrapper } from '../../atoms/header/Header';
@@ -24,26 +25,6 @@ import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
function AtoZ(m1, m2) {
const aName = m1.name;
const bName = m2.name;
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
function sortByPowerLevel(m1, m2) {
const pl1 = m1.powerLevel;
const pl2 = m2.powerLevel;
if (pl1 > pl2) return -1;
if (pl1 < pl2) return 1;
return 0;
}
function simplyfiMembers(members) {
const mx = initMatrix.matrixClient;
return members.map((member) => ({
@@ -111,7 +92,7 @@ function PeopleDrawer({ roomId }) {
setMemberList(
simplyfiMembers(
getMembersWithMembership(membership)
.sort(AtoZ).sort(sortByPowerLevel),
.sort(memberByAtoZ).sort(memberByPowerLevel),
),
);
};
+1 -1
View File
@@ -26,7 +26,7 @@ function Room() {
roomInfo.roomTimeline?.removeInternalListeners();
if (mx.getRoom(rId)) {
setRoomInfo({
roomTimeline: new RoomTimeline(rId, initMatrix.notifications),
roomTimeline: new RoomTimeline(rId),
eventId: eId ?? null,
});
} else {
@@ -21,6 +21,7 @@ import AsyncSearch from '../../../util/AsyncSearch';
import Text from '../../atoms/text/Text';
import ScrollView from '../../atoms/scroll/ScrollView';
import FollowingMembers from '../../molecules/following-members/FollowingMembers';
import { addRecentEmoji } from '../emoji-board/recent';
const commands = [{
name: 'markdown',
@@ -237,6 +238,7 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
viewEvent.emit('cmd_fired');
}
if (myCmd.prefix === ':') {
if (!myCmd.result.mxc) addRecentEmoji(myCmd.result.unicode);
viewEvent.emit('cmd_fired', {
replace: myCmd.result.mxc ? `:${myCmd.result.shortcode}: ` : myCmd.result.unicode,
});
+7 -6
View File
@@ -14,6 +14,7 @@ import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import { openProfileViewer } from '../../../client/action/navigation';
import { diffMinutes, isInSameDay, Throttle } from '../../../util/common';
import { markAsRead } from '../../../client/action/notifications';
import Divider from '../../atoms/divider/Divider';
import ScrollView from '../../atoms/scroll/ScrollView';
@@ -253,7 +254,7 @@ function useHandleScroll(
);
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
if (isAtBottom && readUptoEvtStore.getItem()) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
}
});
autoPaginate();
@@ -263,7 +264,7 @@ function useHandleScroll(
const timelineScroll = timelineScrollRef.current;
const limit = eventLimitRef.current;
if (readUptoEvtStore.getItem()) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
}
if (roomTimeline.isServingLiveTimeline()) {
limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
@@ -286,7 +287,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
const limit = eventLimitRef.current;
const trySendReadReceipt = (event) => {
if (myUserId === event.getSender()) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
return;
}
const readUpToEvent = readUptoEvtStore.getItem();
@@ -295,7 +296,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
if (isUnread === false) {
if (document.visibilityState === 'visible' && timelineScroll.bottom < 16) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
} else {
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
}
@@ -305,7 +306,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
const { timeline } = roomTimeline;
const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToId;
if (unreadMsgIsLast) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
}
};
@@ -399,7 +400,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) {
const readUpToId = roomTimeline.getReadUpToEventId();
if (readUptoEvtStore.getItem()?.getId() === readUpToId || readUpToId === null) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
}
}
jumpToItemIndex = -1;
+15 -20
View File
@@ -5,12 +5,13 @@ import './RoomViewFloating.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { markAsRead } from '../../../client/action/notifications';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import IconButton from '../../atoms/button/IconButton';
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
import MessageIC from '../../../../public/res/ic/outlined/message.svg';
import MessageUnreadIC from '../../../../public/res/ic/outlined/message-unread.svg';
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import { getUsersActionJsx } from './common';
@@ -23,7 +24,7 @@ function useJumpToEvent(roomTimeline) {
};
const cancelJumpToEvent = () => {
roomTimeline.markAllAsRead();
markAsRead(roomTimeline.roomId);
setEventId(null);
};
@@ -36,11 +37,12 @@ function useJumpToEvent(roomTimeline) {
setEventId(readEventId);
}
const { notifications } = initMatrix;
const handleMarkAsRead = () => setEventId(null);
roomTimeline.on(cons.events.roomTimeline.MARKED_AS_READ, handleMarkAsRead);
notifications.on(cons.events.notifications.FULL_READ, handleMarkAsRead);
return () => {
roomTimeline.removeListener(cons.events.roomTimeline.MARKED_AS_READ, handleMarkAsRead);
notifications.removeListener(cons.events.notifications.FULL_READ, handleMarkAsRead);
setEventId(null);
};
}, [roomTimeline]);
@@ -96,28 +98,21 @@ function RoomViewFloating({
return (
<>
<div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}>
<Button onClick={jumpToEvent} variant="primary">
<Text variant="b2">Jump to unread</Text>
<Button iconSrc={MessageUnreadIC} onClick={jumpToEvent} variant="primary">
<Text variant="b3" weight="medium">Jump to unread messages</Text>
</Button>
<Button iconSrc={TickMarkIC} onClick={cancelJumpToEvent} variant="primary">
<Text variant="b3" weight="bold">Mark as read</Text>
</Button>
<IconButton
onClick={cancelJumpToEvent}
variant="primary"
size="extra-small"
src={TickMarkIC}
tooltipPlacement="bottom"
tooltip="Mark as read"
/>
</div>
<div className={`room-view__typing${typingMembers.size > 0 ? ' room-view__typing--open' : ''}`}>
<div className="bouncing-loader"><div /></div>
<Text variant="b2">{getUsersActionJsx(roomId, [...typingMembers], 'typing...')}</Text>
</div>
<div className={`room-view__STB${isAtBottom ? '' : ' room-view__STB--open'}`}>
<IconButton
onClick={handleScrollToBottom}
src={ChevronBottomIC}
tooltip="Scroll to Bottom"
/>
<Button iconSrc={MessageIC} onClick={handleScrollToBottom}>
<Text variant="b3" weight="medium">Jump to latest</Text>
</Button>
</div>
</>
);
+27 -24
View File
@@ -72,51 +72,54 @@
}
}
&__STB,
&__unread {
overflow: hidden;
background-color: var(--bg-surface-low);
border-radius: var(--bo-radius);
& button {
justify-content: flex-start;
border-radius: 0;
box-shadow: none;
padding: 6px var(--sp-tight);
& .ic-raw {
width: 16px;
height: 16px;
}
}
}
&__STB {
position: absolute;
@include dir.prop(right, var(--sp-normal), unset);
@include dir.prop(left, unset, var(--sp-normal));
@include dir.prop(left, 50%, unset);
@include dir.prop(right, unset, 50%);
bottom: 0;
border-radius: var(--bo-radius);
box-shadow: var(--bs-surface-border);
background-color: var(--bg-surface-low);
transition: transform 200ms ease-in-out;
transform: translateY(100%) scale(0);
transform: translate(-50%, 100%);
&--open {
transform: translateY(-28px) scale(1);
transform: translate(-50%, -28px);
}
}
&__unread {
position: absolute;
top: var(--sp-extra-tight);
@include dir.prop(right, var(--sp-extra-tight), unset);
@include dir.prop(left, unset, var(--sp-extra-tight));
@include dir.prop(left, var(--sp-normal), unset);
@include dir.prop(right, unset, var(--sp-normal));
z-index: 999;
display: none;
background-color: var(--bg-surface);
border-radius: var(--bo-radius);
box-shadow: var(--bs-primary-border);
overflow: hidden;
width: calc(100% - var(--sp-extra-loose));
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 20%);
&--open {
display: flex;
}
& .ic-btn {
padding: 6px var(--sp-extra-tight);
border-radius: 0;
}
& .btn-primary {
& button:first-child {
@extend .cp-fx__item-one;
@include dir.side(margin, 0, 1px);
border-radius: 0;
padding: 0 var(--sp-tight);
&:focus {
background-color: var(--bg-primary-hover);
}
}
}
}
+30 -33
View File
@@ -7,6 +7,7 @@ import navigation from '../../../client/state/navigation';
import AsyncSearch from '../../../util/AsyncSearch';
import { selectRoom, selectTab } from '../../../client/action/navigation';
import { joinRuleToIconSrc } from '../../../util/matrixUtil';
import { roomIdByActivity } from '../../../util/sort';
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
@@ -47,27 +48,24 @@ function useVisiblityToggle(setResult) {
return [isOpen, requestClose];
}
function mapRoomIds(roomIds, type) {
function mapRoomIds(roomIds) {
const mx = initMatrix.matrixClient;
const { directs, roomIdToParents } = initMatrix.roomList;
return roomIds.map((roomId) => {
let roomType = type;
if (!roomType) {
roomType = directs.has(roomId) ? 'direct' : 'room';
}
const room = mx.getRoom(roomId);
const parentSet = roomIdToParents.get(roomId);
const parentNames = parentSet
? [...parentSet].map((parentId) => mx.getRoom(parentId).name)
: undefined;
const parentNames = parentSet ? [] : undefined;
parentSet?.forEach((parentId) => parentNames.push(mx.getRoom(parentId).name));
const parents = parentNames ? parentNames.join(', ') : null;
let type = 'room';
if (room.isSpaceRoom()) type = 'space';
else if (directs.has(roomId)) type = 'direct';
return ({
type: roomType,
type,
name: room.name,
parents,
roomId,
@@ -93,34 +91,32 @@ function Search() {
const generateResults = (term) => {
const prefix = term.match(/^[#@*]/)?.[0];
if (term.length === 1) {
const { roomList } = initMatrix;
const spaces = mapRoomIds([...roomList.spaces], 'space').reverse();
const rooms = mapRoomIds([...roomList.rooms], 'room').reverse();
const directs = mapRoomIds([...roomList.directs], 'direct').reverse();
if (prefix === '*') {
asyncSearch.setup(spaces, { keys: 'name', isContain: true, limit: 20 });
handleSearchResults(spaces, '*');
} else if (prefix === '#') {
asyncSearch.setup(rooms, { keys: 'name', isContain: true, limit: 20 });
handleSearchResults(rooms, '#');
} else if (prefix === '@') {
asyncSearch.setup(directs, { keys: 'name', isContain: true, limit: 20 });
handleSearchResults(directs, '@');
} else {
const dataList = spaces.concat(rooms, directs);
asyncSearch.setup(dataList, { keys: 'name', isContain: true, limit: 20 });
asyncSearch.search(term);
}
} else {
if (term.length > 1) {
asyncSearch.search(prefix ? term.slice(1) : term);
return;
}
const { spaces, rooms, directs } = initMatrix.roomList;
let ids = null;
if (prefix) {
if (prefix === '#') ids = [...rooms];
else if (prefix === '@') ids = [...directs];
else ids = [...spaces];
} else {
ids = [...rooms].concat([...directs], [...spaces]);
}
ids.sort(roomIdByActivity);
const mappedIds = mapRoomIds(ids);
asyncSearch.setup(mappedIds, { keys: 'name', isContain: true, limit: 20 });
if (prefix) handleSearchResults(mappedIds, prefix);
else asyncSearch.search(term);
};
const loadRecentRooms = () => {
const { recentRooms } = navigation;
handleSearchResults(mapRoomIds(recentRooms).reverse(), '');
handleSearchResults(mapRoomIds(recentRooms).reverse());
};
const handleAfterOpen = () => {
@@ -154,6 +150,7 @@ function Search() {
else {
searchRef.current.value = '';
searchRef.current.focus();
loadRecentRooms();
}
};
+219 -194
View File
@@ -1,13 +1,13 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import React, { useState, useEffect } from 'react';
import './Settings.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import settings from '../../../client/state/settings';
import navigation from '../../../client/state/navigation';
import {
toggleSystemTheme, toggleMarkdown, toggleMembershipEvents, toggleNickAvatarEvents,
toggleNotifications,
toggleNotifications, toggleNotificationSounds,
} from '../../../client/action/settings';
import logout from '../../../client/action/logout';
import { usePermission } from '../../hooks/usePermission';
@@ -16,16 +16,17 @@ import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import Toggle from '../../atoms/button/Toggle';
import Tabs from '../../atoms/tabs/Tabs';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import SegmentedControls from '../../atoms/segmented-controls/SegmentedControls';
import PopupWindow, { PWContentSelector } from '../../molecules/popup-window/PopupWindow';
import PopupWindow from '../../molecules/popup-window/PopupWindow';
import SettingTile from '../../molecules/setting-tile/SettingTile';
import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ImportE2ERoomKeys';
import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
import ProfileEditor from '../profile-editor/ProfileEditor';
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
import SunIC from '../../../../public/res/ic/outlined/sun.svg';
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
import BellIC from '../../../../public/res/ic/outlined/bell.svg';
@@ -35,85 +36,74 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import CinnySVG from '../../../../public/res/svg/cinny.svg';
function GeneralSection() {
return (
<div className="settings-content">
<SettingTile
title=""
content={(
<ProfileEditor userId={initMatrix.matrixClient.getUserId()} />
)}
/>
</div>
);
}
function AppearanceSection() {
const [, updateState] = useState({});
return (
<div className="settings-content">
<SettingTile
title="Follow system theme"
options={(
<Toggle
isActive={settings.useSystemTheme}
onToggle={() => { toggleSystemTheme(); updateState({}); }}
/>
)}
content={<Text variant="b3">Use light or dark mode based on the system's settings.</Text>}
/>
{(() => {
if (!settings.useSystemTheme) {
return (
<SettingTile
title="Theme"
content={(
<SegmentedControls
selected={settings.getThemeIndex()}
segments={[
{ text: 'Light' },
{ text: 'Silver' },
{ text: 'Dark' },
{ text: 'Butter' },
]}
onSelect={(index) => settings.setTheme(index)}
/>
)}
<div className="settings-appearance">
<div className="settings-appearance__card">
<MenuHeader>Theme</MenuHeader>
<SettingTile
title="Follow system theme"
options={(
<Toggle
isActive={settings.useSystemTheme}
onToggle={() => { toggleSystemTheme(); updateState({}); }}
/>
);
}
})()}
<SettingTile
title="Markdown formatting"
options={(
<Toggle
isActive={settings.isMarkdown}
onToggle={() => { toggleMarkdown(); updateState({}); }}
)}
content={<Text variant="b3">Use light or dark mode based on the system settings.</Text>}
/>
{!settings.useSystemTheme && (
<SettingTile
title="Theme"
content={(
<SegmentedControls
selected={settings.getThemeIndex()}
segments={[
{ text: 'Light' },
{ text: 'Silver' },
{ text: 'Dark' },
{ text: 'Butter' },
]}
onSelect={(index) => settings.setTheme(index)}
/>
)}
/>
)}
content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>}
/>
<SettingTile
title="Hide membership events"
options={(
<Toggle
isActive={settings.hideMembershipEvents}
onToggle={() => { toggleMembershipEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)</Text>}
/>
<SettingTile
title="Hide nick/avatar events"
options={(
<Toggle
isActive={settings.hideNickAvatarEvents}
onToggle={() => { toggleNickAvatarEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>}
/>
</div>
<div className="settings-appearance__card">
<MenuHeader>Room messages</MenuHeader>
<SettingTile
title="Markdown formatting"
options={(
<Toggle
isActive={settings.isMarkdown}
onToggle={() => { toggleMarkdown(); updateState({}); }}
/>
)}
content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>}
/>
<SettingTile
title="Hide membership events"
options={(
<Toggle
isActive={settings.hideMembershipEvents}
onToggle={() => { toggleMembershipEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)</Text>}
/>
<SettingTile
title="Hide nick/avatar events"
options={(
<Toggle
isActive={settings.hideNickAvatarEvents}
onToggle={() => { toggleNickAvatarEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>}
/>
</div>
</div>
);
}
@@ -125,7 +115,7 @@ function NotificationsSection() {
const renderOptions = () => {
if (window.Notification === undefined) {
return <Text className="set-notifications__not-supported">Not supported in this browser.</Text>;
return <Text className="settings-notifications__not-supported">Not supported in this browser.</Text>;
}
if (permission === 'granted') {
@@ -152,11 +142,22 @@ function NotificationsSection() {
};
return (
<div className="set-notifications settings-content">
<div className="settings-notifications">
<MenuHeader>Notification & Sound</MenuHeader>
<SettingTile
title="Show desktop notifications"
title="Desktop notification"
options={renderOptions()}
content={<Text variant="b3">Show notifications when new messages arrive.</Text>}
content={<Text variant="b3">Show desktop notification when new messages arrive.</Text>}
/>
<SettingTile
title="Notification Sound"
options={(
<Toggle
isActive={settings.isNotificationSounds}
onToggle={() => { toggleNotificationSounds(); updateState({}); }}
/>
)}
content={<Text variant="b3">Play sound when new messages arrive.</Text>}
/>
</div>
);
@@ -164,149 +165,173 @@ function NotificationsSection() {
function SecuritySection() {
return (
<div className="set-security settings-content">
<SettingTile
title={`Device ID: ${initMatrix.matrixClient.getDeviceId()}`}
/>
<SettingTile
title={`Device key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
content={<Text variant="b3">Use this device ID-key combo to verify or manage this session from Element client.</Text>}
/>
<SettingTile
title="Export E2E room keys"
content={(
<>
<Text variant="b3">Export end-to-end encryption room keys to decrypt old messages in other session. In order to encrypt keys you need to set a password, which will be used while importing.</Text>
<ExportE2ERoomKeys />
</>
)}
/>
<SettingTile
title="Import E2E room keys"
content={(
<>
<Text variant="b3">{'To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you\'ll have to enter the password you set in order to decrypt it.'}</Text>
<ImportE2ERoomKeys />
</>
)}
/>
<div className="settings-security">
<div className="settings-security__card">
<MenuHeader>Device Info</MenuHeader>
<SettingTile
title={`Device ID: ${initMatrix.matrixClient.getDeviceId()}`}
/>
<SettingTile
title={`Device key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
content={<Text variant="b3">Use this device ID-key combo to verify or manage this session from Element client.</Text>}
/>
</div>
<div className="settings-security__card">
<MenuHeader>Encryption</MenuHeader>
<SettingTile
title="Export E2E room keys"
content={(
<>
<Text variant="b3">Export end-to-end encryption room keys to decrypt old messages in other session. In order to encrypt keys you need to set a password, which will be used while importing.</Text>
<ExportE2ERoomKeys />
</>
)}
/>
<SettingTile
title="Import E2E room keys"
content={(
<>
<Text variant="b3">{'To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you\'ll have to enter the password you set in order to decrypt it.'}</Text>
<ImportE2ERoomKeys />
</>
)}
/>
</div>
</div>
);
}
function AboutSection() {
return (
<div className="settings-content set__about">
<div className="set-about__branding">
<img width="60" height="60" src={CinnySVG} alt="Cinny logo" />
<div>
<Text variant="h2" weight='medium'>
Cinny
<span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span>
</Text>
<Text>Yet another matrix client</Text>
<div className="settings-about">
<div className="settings-about__card">
<MenuHeader>Application</MenuHeader>
<div className="settings-about__branding">
<img width="60" height="60" src={CinnySVG} alt="Cinny logo" />
<div>
<Text variant="h2" weight="medium">
Cinny
<span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span>
</Text>
<Text>Yet another matrix client</Text>
<div className="set-about__btns">
<Button onClick={() => window.open('https://github.com/ajbura/cinny')}>Source code</Button>
<Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button>
<div className="settings-about__btns">
<Button onClick={() => window.open('https://github.com/ajbura/cinny')}>Source code</Button>
<Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button>
</div>
</div>
</div>
</div>
<div className="set-about__credits">
<Text variant="s1" weight="medium">Credits</Text>
<ul>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://github.com/matrix-org/matrix-js-sdk" rel="noreferrer noopener" target="_blank">matrix-js-sdk</a> is © <a href="https://matrix.org/foundation" rel="noreferrer noopener" target="_blank">The Matrix.org Foundation C.I.C</a> used under the terms of <a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">Apache 2.0</a>.</Text>
</li>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twemoji</a> emoji art is © <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twitter, Inc and other contributors</a> used under the terms of <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer noopener">CC-BY 4.0</a>.</Text>
</li>
</ul>
<div className="settings-about__card">
<MenuHeader>Credits</MenuHeader>
<div className="settings-about__credits">
<ul>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://github.com/matrix-org/matrix-js-sdk" rel="noreferrer noopener" target="_blank">matrix-js-sdk</a> is © <a href="https://matrix.org/foundation" rel="noreferrer noopener" target="_blank">The Matrix.org Foundation C.I.C</a> used under the terms of <a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">Apache 2.0</a>.</Text>
</li>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twemoji</a> emoji art is © <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twitter, Inc and other contributors</a> used under the terms of <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer noopener">CC-BY 4.0</a>.</Text>
</li>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://material.io/design/sound/sound-resources.html" target="_blank" rel="noreferrer noopener">Material sound resources</a> are © <a href="https://google.com" target="_blank" rel="noreferrer noopener">Google</a> used under the terms of <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer noopener">CC-BY 4.0</a>.</Text>
</li>
</ul>
</div>
</div>
</div>
);
}
function Settings({ isOpen, onRequestClose }) {
const settingSections = [{
name: 'General',
iconSrc: SettingsIC,
render() {
return <GeneralSection />;
},
}, {
name: 'Appearance',
iconSrc: SunIC,
render() {
return <AppearanceSection />;
},
}, {
name: 'Notifications',
iconSrc: BellIC,
render() {
return <NotificationsSection />;
},
}, {
name: 'Security & Privacy',
iconSrc: LockIC,
render() {
return <SecuritySection />;
},
}, {
name: 'Help & About',
iconSrc: InfoIC,
render() {
return <AboutSection />;
},
}];
const [selectedSection, setSelectedSection] = useState(settingSections[0]);
const tabText = {
APPEARANCE: 'Appearance',
NOTIFICATIONS: 'Notifications',
SECURITY: 'Security',
ABOUT: 'About',
};
const tabItems = [{
text: tabText.APPEARANCE,
iconSrc: SunIC,
disabled: false,
render: () => <AppearanceSection />,
}, {
text: tabText.NOTIFICATIONS,
iconSrc: BellIC,
disabled: false,
render: () => <NotificationsSection />,
}, {
text: tabText.SECURITY,
iconSrc: LockIC,
disabled: false,
render: () => <SecuritySection />,
}, {
text: tabText.ABOUT,
iconSrc: InfoIC,
disabled: false,
render: () => <AboutSection />,
}];
function useWindowToggle(setSelectedTab) {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const openSettings = (tab) => {
const tabItem = tabItems.find((item) => item.text === tab);
if (tabItem) setSelectedTab(tabItem);
setIsOpen(true);
};
navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
return () => {
navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings);
};
}, []);
const requestClose = () => setIsOpen(false);
return [isOpen, requestClose];
}
function Settings() {
const [selectedTab, setSelectedTab] = useState(tabItems[0]);
const [isOpen, requestClose] = useWindowToggle(setSelectedTab);
const handleTabChange = (tabItem) => setSelectedTab(tabItem);
const handleLogout = () => {
if (confirm('Confirm logout')) logout();
};
return (
<PopupWindow
className="settings-window"
isOpen={isOpen}
onRequestClose={onRequestClose}
title="Settings"
contentTitle={selectedSection.name}
drawer={(
className="settings-window"
title={<Text variant="s1" weight="medium" primary>Settings</Text>}
contentOptions={(
<>
{
settingSections.map((section) => (
<PWContentSelector
key={section.name}
selected={selectedSection.name === section.name}
onClick={() => setSelectedSection(section)}
iconSrc={section.iconSrc}
>
{section.name}
</PWContentSelector>
))
}
<PWContentSelector
variant="danger"
onClick={handleLogout}
iconSrc={PowerIC}
>
<Button variant="danger" iconSrc={PowerIC} onClick={handleLogout}>
Logout
</PWContentSelector>
</Button>
<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />
</>
)}
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
onRequestClose={requestClose}
>
{selectedSection.render()}
{isOpen && (
<div className="settings-window__content">
<ProfileEditor userId={initMatrix.matrixClient.getUserId()} />
<Tabs
items={tabItems}
defaultSelected={tabItems.findIndex((tab) => tab.text === selectedTab.text)}
onSelect={handleTabChange}
/>
<div className="settings-window__cards-wrapper">
{ selectedTab.render() }
</div>
</div>
)}
</PopupWindow>
);
}
Settings.propTypes = {
isOpen: PropTypes.bool.isRequired,
onRequestClose: PropTypes.func.isRequired,
};
export default Settings;
+59 -31
View File
@@ -2,58 +2,86 @@
@use '../../partials/dir';
.settings-window {
& .pw__drawer__content {
@extend .cp-fx__column;
min-height: 100%;
padding-bottom: var(--sp-extra-tight);
& .pw {
background-color: var(--bg-surface-low);
}
.header .btn-danger {
margin: 0 var(--sp-tight);
box-shadow: none;
}
& > .pw-content-selector:last-child {
margin-top: auto;
& .profile-editor {
padding: var(--sp-loose) var(--sp-extra-loose);
}
& .tabs__content {
padding: 0 var(--sp-normal);
}
&__cards-wrapper {
padding: 0 var(--sp-normal);
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
}
}
.settings-window__card {
margin: var(--sp-normal) 0;
background-color: var(--bg-surface);
border-radius: var(--bo-radius);
box-shadow: var(--bs-surface-border);
overflow: hidden;
& > .context-menu__header:first-child {
margin-top: 2px;
}
}
.settings-appearance__card,
.settings-notifications,
.settings-security__card,
.settings-about__card {
@extend .settings-window__card;
}
.settings-window__cards-wrapper{
& .setting-tile {
margin: 0 var(--sp-normal);
margin-top: var(--sp-normal);
padding-bottom: 16px;
border-bottom: 1px solid var(--bg-surface-border);
&:last-child {
border-bottom: none;
}
}
& .pw__content-container {
min-height: 100%;
}
}
.settings-content {
@include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
& .setting-tile {
margin-top: var(--sp-normal);
border-bottom: 1px solid var(--bg-surface-border);
padding-bottom: 16px;
}
}
.set-notifications {
.settings-notifications {
&__not-supported {
padding: 0 var(--sp-ultra-tight);
}
}
.set-about {
.settings-about {
&__branding {
margin-top: var(--sp-extra-tight);
margin-bottom: var(--sp-normal);
padding: var(--sp-normal);
display: flex;
& > div {
margin: 0 var(--sp-loose);
}
}
&__btns {
margin: 0;
margin-top: var(--sp-normal);
& button:last-child {
margin: 0 var(--sp-tight)
& button {
margin-top: var(--sp-tight);
@include dir.side(margin, 0, var(--sp-tight));
}
}
&__credits {
margin-top: var(--sp-loose);
padding: 0 var(--sp-normal);
& ul {
color: var(--tc-surface-low);
padding: var(--sp-normal);
margin: var(--sp-extra-tight) 0;
}
}
@@ -6,6 +6,7 @@ import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/accountData';
import { joinRuleToIconSrc } from '../../../util/matrixUtil';
import { roomIdByAtoZ } from '../../../util/sort';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
@@ -20,7 +21,6 @@ import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useSpaceShortcut } from '../../hooks/useSpaceShortcut';
import { AtoZ } from '../navigation/common';
function ShortcutSpacesContent() {
const mx = initMatrix.matrixClient;
@@ -29,7 +29,7 @@ function ShortcutSpacesContent() {
const [spaceShortcut] = useSpaceShortcut();
const spaceWithoutShortcut = [...spaces].filter(
(spaceId) => !spaceShortcut.includes(spaceId),
).sort(AtoZ);
).sort(roomIdByAtoZ);
const [process, setProcess] = useState(null);
const [selected, setSelected] = useState([]);
@@ -7,14 +7,10 @@
& .room-profile {
padding: var(--sp-loose) var(--sp-extra-loose);
}
}
& .tabs {
box-shadow: inset 0 -1px 0 var(--bg-surface-border);
&__content {
padding: 0 var(--sp-normal);
}
& .tabs__content {
padding: 0 var(--sp-normal);
}
&__cards-wrapper {
+5
View File
@@ -1,6 +1,9 @@
import React, { useState, useEffect } from 'react';
import './Client.scss';
import { initHotkeys } from '../../../client/event/hotkeys';
import { initRoomListListener } from '../../../client/event/roomList';
import Text from '../../atoms/text/Text';
import Spinner from '../../atoms/spinner/Spinner';
import Navigation from '../../organisms/navigation/Navigation';
@@ -38,6 +41,8 @@ function Client() {
}, 15000);
initMatrix.once('init_loading_finished', () => {
clearInterval(iId);
initHotkeys();
initRoomListListener(initMatrix.roomList);
changeLoading(false);
});
initMatrix.init();
+2 -1
View File
@@ -95,9 +95,10 @@ export function openProfileViewer(userId, roomId) {
});
}
export function openSettings() {
export function openSettings(tabText) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_SETTINGS,
tabText,
});
}
+26
View File
@@ -0,0 +1,26 @@
import initMatrix from '../initMatrix';
// eslint-disable-next-line import/prefer-default-export
export async function markAsRead(roomId) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
if (!room) return;
initMatrix.notifications.deleteNoti(roomId);
const timeline = room.getLiveTimeline().getEvents();
const readEventId = room.getEventReadUpTo(mx.getUserId());
const getLatestValidEvent = () => {
for (let i = timeline.length - 1; i >= 0; i -= 1) {
const latestEvent = timeline[i];
if (latestEvent.getId() === readEventId) return null;
if (!latestEvent.isSending()) return latestEvent;
}
return null;
};
if (timeline.length === 0) return;
const latestEvent = getLatestValidEvent();
if (latestEvent === null) return;
await mx.sendReadReceipt(latestEvent);
}
+6
View File
@@ -36,3 +36,9 @@ export function toggleNotifications() {
type: cons.actions.settings.TOGGLE_NOTIFICATIONS,
});
}
export function toggleNotificationSounds() {
appDispatcher.dispatch({
type: cons.actions.settings.TOGGLE_NOTIFICATION_SOUNDS,
});
}
+12 -3
View File
@@ -1,5 +1,6 @@
import { openSearch, toggleRoomSettings } from '../action/navigation';
import navigation from '../state/navigation';
import { markAsRead } from '../action/notifications';
function listenKeyboard(event) {
// Ctrl/Cmd +
@@ -18,11 +19,19 @@ function listenKeyboard(event) {
return;
}
// esc - close room settings panel
if (event.keyCode === 27 && navigation.isRoomSettings) {
toggleRoomSettings();
// esc
if (event.keyCode === 27) {
if (navigation.isRoomSettings) {
toggleRoomSettings();
return;
}
if (navigation.selectedRoomId) {
markAsRead(navigation.selectedRoomId);
return;
}
}
// Don't allow these keys to type/focus message field
if ((event.keyCode !== 8 && event.keyCode < 48)
|| (event.keyCode >= 91 && event.keyCode <= 93)
|| (event.keyCode >= 112 && event.keyCode <= 183)) {
-4
View File
@@ -7,8 +7,6 @@ import RoomList from './state/RoomList';
import AccountData from './state/AccountData';
import RoomsInput from './state/RoomsInput';
import Notifications from './state/Notifications';
import { initHotkeys } from './event/hotkeys';
import { initRoomListListener } from './event/roomList';
global.Olm = require('@matrix-org/olm');
@@ -66,8 +64,6 @@ class InitMatrix extends EventEmitter {
this.accountData = new AccountData(this.roomList);
this.roomsInput = new RoomsInput(this.matrixClient);
this.notifications = new Notifications(this.roomList);
initHotkeys();
initRoomListListener(this.roomList);
this.emit('init_loading_finished');
}
},
+48 -20
View File
@@ -6,6 +6,9 @@ import cons from './cons';
import navigation from './navigation';
import settings from './settings';
import NotificationSound from '../../../public/sound/notification.ogg';
import InviteSound from '../../../public/sound/invite.ogg';
function isNotifEvent(mEvent) {
const eType = mEvent.getType();
if (!cons.supportEventTypes.includes(eType)) return false;
@@ -185,7 +188,7 @@ class Notifications extends EventEmitter {
}
async _displayPopupNoti(mEvent, room) {
if (!settings.showNotifications) return;
if (!settings.showNotifications && !settings.isNotificationSounds) return;
const actions = this.matrixClient.getPushActionsForEvent(mEvent);
if (!actions?.notify) return;
@@ -196,28 +199,50 @@ class Notifications extends EventEmitter {
await mEvent.attemptDecryption(this.matrixClient.crypto);
}
let title;
if (!mEvent.sender || room.name === mEvent.sender.name) {
title = room.name;
} else if (mEvent.sender) {
title = `${mEvent.sender.name} (${room.name})`;
if (settings.showNotifications) {
let title;
if (!mEvent.sender || room.name === mEvent.sender.name) {
title = room.name;
} else if (mEvent.sender) {
title = `${mEvent.sender.name} (${room.name})`;
}
const iconSize = 36;
const icon = await renderAvatar({
text: mEvent.sender.name,
bgColor: cssColorMXID(mEvent.getSender()),
imageSrc: mEvent.sender?.getAvatarUrl(this.matrixClient.baseUrl, iconSize, iconSize, 'crop'),
size: iconSize,
borderRadius: 8,
scale: 8,
});
const noti = new window.Notification(title, {
body: mEvent.getContent().body,
icon,
silent: settings.isNotificationSounds,
});
if (settings.isNotificationSounds) {
noti.onshow = () => this._playNotiSound();
}
noti.onclick = () => selectRoom(room.roomId, mEvent.getId());
} else {
this._playNotiSound();
}
}
const iconSize = 36;
const icon = await renderAvatar({
text: mEvent.sender.name,
bgColor: cssColorMXID(mEvent.getSender()),
imageSrc: mEvent.sender?.getAvatarUrl(this.matrixClient.baseUrl, iconSize, iconSize, 'crop'),
size: iconSize,
borderRadius: 8,
scale: 8,
});
_playNotiSound() {
if (!this._notiAudio) {
this._notiAudio = new Audio(NotificationSound);
}
this._notiAudio.play();
}
const noti = new window.Notification(title, {
body: mEvent.getContent().body,
icon,
});
noti.onclick = () => selectRoom(room.roomId, mEvent.getId());
_playInviteSound() {
if (!this._inviteAudio) {
this._inviteAudio = new Audio(InviteSound);
}
this._inviteAudio.play();
}
_listenEvents() {
@@ -299,6 +324,9 @@ class Notifications extends EventEmitter {
if (membership === 'leave' && this.hasNoti(room.roomId)) {
this.deleteNoti(room.roomId);
}
if (membership === 'invite') {
this._playInviteSound();
}
});
}
}
+1 -21
View File
@@ -77,7 +77,7 @@ function isTimelineLinked(tm1, tm2) {
}
class RoomTimeline extends EventEmitter {
constructor(roomId, notifications) {
constructor(roomId) {
super();
// These are local timelines
this.timeline = [];
@@ -88,7 +88,6 @@ class RoomTimeline extends EventEmitter {
this.matrixClient = initMatrix.matrixClient;
this.roomId = roomId;
this.room = this.matrixClient.getRoom(roomId);
this.notifications = notifications;
this.liveTimeline = this.room.getLiveTimeline();
this.activeTimeline = this.liveTimeline;
@@ -228,25 +227,6 @@ class RoomTimeline extends EventEmitter {
return Promise.allSettled(decryptionPromises);
}
markAllAsRead() {
const readEventId = this.getReadUpToEventId();
const getLatestValidEvent = () => {
for (let i = this.timeline.length - 1; i >= 0; i -= 1) {
const latestEvent = this.timeline[i];
if (latestEvent.getId() === readEventId) return null;
if (!latestEvent.isSending()) return latestEvent;
}
return null;
};
this.notifications.deleteNoti(this.roomId);
if (this.timeline.length === 0) return;
const latestEvent = getLatestValidEvent();
if (latestEvent === null) return;
if (readEventId === latestEvent.getId()) return;
this.matrixClient.sendReadReceipt(latestEvent);
this.emit(cons.events.roomTimeline.MARKED_AS_READ, latestEvent);
}
hasEventInTimeline(eventId, timeline = this.activeTimeline) {
const timelineSet = this.getUnfilteredTimelineSet();
const eventTimeline = timelineSet.getTimelineForEvent(eventId);
+2 -2
View File
@@ -97,13 +97,13 @@ function getFormattedBody(markdown) {
function getReplyFormattedBody(roomId, reply) {
const replyToLink = `<a href="https://matrix.to/#/${roomId}/${reply.eventId}">In reply to</a>`;
const userLink = `<a href="https://matrix.to/#/${reply.userId}">${reply.userId}</a>`;
const formattedReply = getFormattedBody(reply.body.replaceAll('\n', '\n> '));
const formattedReply = getFormattedBody(reply.body.replace(/\n/g, '\n> '));
return `<mx-reply><blockquote>${replyToLink}${userLink}<br />${formattedReply}</blockquote></mx-reply>`;
}
function bindReplyToContent(roomId, reply, content) {
const newContent = { ...content };
newContent.body = `> <${reply.userId}> ${reply.body.replaceAll('\n', '\n> ')}`;
newContent.body = `> <${reply.userId}> ${reply.body.replace(/\n/g, '\n> ')}`;
newContent.body += `\n\n${content.body}`;
newContent.format = 'org.matrix.custom.html';
newContent['m.relates_to'] = content['m.relates_to'] || {};
+2 -1
View File
@@ -67,6 +67,7 @@ const cons = {
TOGGLE_MEMBERSHIP_EVENT: 'TOGGLE_MEMBERSHIP_EVENT',
TOGGLE_NICKAVATAR_EVENT: 'TOGGLE_NICKAVATAR_EVENT',
TOGGLE_NOTIFICATIONS: 'TOGGLE_NOTIFICATIONS',
TOGGLE_NOTIFICATION_SOUNDS: 'TOGGLE_NOTIFICATION_SOUNDS',
},
},
events: {
@@ -115,7 +116,6 @@ const cons = {
PAGINATED: 'PAGINATED',
TYPING_MEMBERS_UPDATED: 'TYPING_MEMBERS_UPDATED',
LIVE_RECEIPT: 'LIVE_RECEIPT',
MARKED_AS_READ: 'MARKED_AS_READ',
EVENT_REDACTED: 'EVENT_REDACTED',
AT_BOTTOM: 'AT_BOTTOM',
SCROLL_TO_LIVE: 'SCROLL_TO_LIVE',
@@ -135,6 +135,7 @@ const cons = {
MEMBERSHIP_EVENTS_TOGGLED: 'MEMBERSHIP_EVENTS_TOGGLED',
NICKAVATAR_EVENTS_TOGGLED: 'NICKAVATAR_EVENTS_TOGGLED',
NOTIFICATIONS_TOGGLED: 'NOTIFICATIONS_TOGGLED',
NOTIFICATION_SOUNDS_TOGGLED: 'NOTIFICATION_SOUNDS_TOGGLED',
},
},
};
+3 -2
View File
@@ -129,12 +129,13 @@ class Navigation extends EventEmitter {
this.emit(cons.events.navigation.PROFILE_VIEWER_OPENED, action.userId, action.roomId);
},
[cons.actions.navigation.OPEN_SETTINGS]: () => {
this.emit(cons.events.navigation.SETTINGS_OPENED);
this.emit(cons.events.navigation.SETTINGS_OPENED, action.tabText);
},
[cons.actions.navigation.OPEN_EMOJIBOARD]: () => {
this.emit(
cons.events.navigation.EMOJIBOARD_OPENED,
action.cords, action.requestEmojiCallback,
action.cords,
action.requestEmojiCallback,
);
},
[cons.actions.navigation.OPEN_READRECEIPTS]: () => {
+15
View File
@@ -29,6 +29,7 @@ class Settings extends EventEmitter {
this.hideMembershipEvents = this.getHideMembershipEvents();
this.hideNickAvatarEvents = this.getHideNickAvatarEvents();
this._showNotifications = this.getShowNotifications();
this.isNotificationSounds = this.getIsNotificationSounds();
this.isTouchScreenDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0);
}
@@ -125,6 +126,15 @@ class Settings extends EventEmitter {
return settings.showNotifications;
}
getIsNotificationSounds() {
if (typeof this.isNotificationSounds === 'boolean') return this.isNotificationSounds;
const settings = getSettings();
if (settings === null) return true;
if (typeof settings.isNotificationSounds === 'undefined') return true;
return settings.isNotificationSounds;
}
setter(action) {
const actions = {
[cons.actions.settings.TOGGLE_SYSTEM_THEME]: () => {
@@ -164,6 +174,11 @@ class Settings extends EventEmitter {
setSettings('showNotifications', this._showNotifications);
this.emit(cons.events.settings.NOTIFICATIONS_TOGGLED, this._showNotifications);
},
[cons.actions.settings.TOGGLE_NOTIFICATION_SOUNDS]: () => {
this.isNotificationSounds = !this.isNotificationSounds;
setSettings('isNotificationSounds', this.isNotificationSounds);
this.emit(cons.events.settings.NOTIFICATION_SOUNDS_TOGGLED, this.isNotificationSounds);
},
};
actions[action.type]?.();
+1 -1
View File
@@ -123,7 +123,7 @@ class AsyncSearch extends EventEmitter {
_normalize(item) {
let myItem = item.normalize(this.normalizeUnicode ? 'NFKC' : 'NFC');
if (!this.isCaseSensitive) myItem = myItem.toLocaleLowerCase();
if (this.ignoreWhitespace) myItem = myItem.replaceAll(' ', '');
if (this.ignoreWhitespace) myItem = myItem.replace(/\s/g, '');
return myItem;
}
+47
View File
@@ -0,0 +1,47 @@
import initMatrix from '../client/initMatrix';
export function roomIdByActivity(id1, id2) {
const room1 = initMatrix.matrixClient.getRoom(id1);
const room2 = initMatrix.matrixClient.getRoom(id2);
return room2.getLastActiveTimestamp() - room1.getLastActiveTimestamp();
}
export function roomIdByAtoZ(aId, bId) {
let aName = initMatrix.matrixClient.getRoom(aId).name;
let bName = initMatrix.matrixClient.getRoom(bId).name;
// remove "#" from the room name
// To ignore it in sorting
aName = aName.replaceAll('#', '');
bName = bName.replaceAll('#', '');
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
export function memberByAtoZ(m1, m2) {
const aName = m1.name;
const bName = m2.name;
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
export function memberByPowerLevel(m1, m2) {
const pl1 = m1.powerLevel;
const pl2 = m2.powerLevel;
if (pl1 > pl2) return -1;
if (pl1 < pl2) return 1;
return 0;
}
+1 -1
View File
@@ -39,7 +39,7 @@ module.exports = {
use: ['html-loader'],
},
{
test: /\.(png|jpe?g|gif|otf|ttf|woff|woff2)$/,
test: /\.(png|jpe?g|gif|otf|ttf|woff|woff2|ogg)$/,
type: 'asset/resource',
},
{