Skip to content

Commit

Permalink
Merge stable into release
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmz committed Jul 4, 2024
2 parents cac1b75 + 259e567 commit 052015c
Show file tree
Hide file tree
Showing 39 changed files with 3,489 additions and 2,441 deletions.
893 changes: 0 additions & 893 deletions .yarn/releases/yarn-4.1.1.cjs

This file was deleted.

894 changes: 894 additions & 0 deletions .yarn/releases/yarn-4.3.1.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-4.1.1.cjs
yarnPath: .yarn/releases/yarn-4.3.1.cjs
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.133.0] - Not released
### Added
- Add a "description" meta-tag to the index.html.
- Autocomplete for the user/group names in the post and comment inputs. When the
user types "@" and some text afterwards, the matched users/groups are shown
beneath the text input.
### Changed
- Switch to V3 server API (with _omittedCommentsOffset_ field and two comments
after the fold).
### Fixed
- Update SSI patterns in index.html to support dashes in groupnames.
- Bug on iOS that blocks deletion of post's attachments.

## [1.132.0] - 2024-05-27
### Added
- Ability to "unlock" and preview comments from the banned users and with reply
Expand Down
5 changes: 3 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />

<!-- Support for FB OpenGraph and Twitter -->
<!--#if expr="${request_uri} = /^\/([a-zA-Z0-9]+)\/([a-fA-F0-9\-]{36}|[a-fA-F0-9]{4,6})/" -->
<!--#if expr="${request_uri} = /^\/([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)\/([a-fA-F0-9\-]{36}|[a-fA-F0-9]{4,6})/" -->
<!--#set var="post_id" value="$2" -->
<!--#include virtual="/v2/posts-opengraph/${post_id}" -->
<!--#endif -->

<!-- RSS autodiscovery -->
<!--#if expr="${request_uri} = /^\/([a-zA-Z0-9]+)/" -->
<!--#if expr="${request_uri} = /^\/([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)/" -->
<!--#set var="username" value="$1" -->
<!--#include virtual="/v2/timelines-metatags/${username}" -->
<!--#endif -->

<title>FreeFeed</title>
<meta name="description" content="FreeFeed is a small and free social network, that enables you to discover and discuss the interesting stuff your friends find on the web">

<link rel="icon" href="/assets/images/favicon.ico" type="image/x-icon" />
<link rel="icon" href="/assets/images/favicon.svg" type="image/svg+xml" />
Expand Down
52 changes: 27 additions & 25 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "reactive-pepyatka",
"version": "1.132.0",
"version": "1.133.0",
"description": "",
"main": "index.js",
"dependencies": {
Expand All @@ -12,7 +12,7 @@
"classnames": "~2.5.1",
"custom-event": "~1.0.1",
"date-fns": "~3.6.0",
"debug": "~4.3.4",
"debug": "~4.3.5",
"events": "~3.3.0",
"filesize": "~10.1.2",
"final-form": "~4.20.10",
Expand All @@ -22,10 +22,10 @@
"keycode-js": "~3.1.0",
"local-storage-fallback": "~4.1.2",
"lodash-es": "~4.17.21",
"lru-cache": "~10.2.2",
"lru-cache": "~10.3.0",
"memoize-one": "~6.0.0",
"mousetrap": "~1.6.5",
"photoswipe": "~5.4.3",
"photoswipe": "~5.4.4",
"porter-stemmer": "~0.9.1",
"prop-types": "~15.8.1",
"react": "~18.3.1",
Expand All @@ -40,65 +40,67 @@
"react-select": "~5.8.0",
"react-sortablejs": "~6.1.4",
"react-textarea-autosize": "~8.5.3",
"react-use-event-hook": "~0.9.6",
"recharts": "~2.12.7",
"redux": "~5.0.1",
"snarkdown": "~2.0.0",
"social-text-tokenizer": "~3.0.0",
"socket.io-client": "~2.3.1",
"sortablejs": "~1.15.2",
"tabbable": "~6.2.0",
"ua-parser-js": "~1.0.37",
"ua-parser-js": "~1.0.38",
"use-subscription": "~1.8.2",
"validator": "~13.12.0",
"vazirmatn": "^33.0.3",
"whatwg-fetch": "~3.6.20"
},
"devDependencies": {
"@babel/core": "~7.24.5",
"@babel/eslint-parser": "~7.24.5",
"@babel/preset-react": "~7.24.1",
"@babel/core": "~7.24.7",
"@babel/eslint-parser": "~7.24.7",
"@babel/preset-react": "~7.24.7",
"@gfx/zopfli": "~1.0.15",
"@testing-library/jest-dom": "~6.4.5",
"@testing-library/react": "~15.0.7",
"@testing-library/dom": "~10.3.0",
"@testing-library/jest-dom": "~6.4.6",
"@testing-library/react": "~16.0.0",
"@testing-library/react-hooks": "~8.0.1",
"@testing-library/user-event": "~14.5.2",
"@vitejs/plugin-legacy": "~5.4.0",
"@vitejs/plugin-react-swc": "^3.6.0",
"@vitejs/plugin-legacy": "~5.4.1",
"@vitejs/plugin-react-swc": "^3.7.0",
"cross-env": "~7.0.3",
"esbuild": "~0.21.3",
"esbuild": "~0.23.0",
"eslint": "~8.56.0",
"eslint-config-prettier": "~9.1.0",
"eslint-plugin-babel": "~5.3.1",
"eslint-plugin-import": "~2.29.1",
"eslint-plugin-lodash": "~8.0.0",
"eslint-plugin-prettier": "~5.1.3",
"eslint-plugin-promise": "~6.1.1",
"eslint-plugin-react": "~7.34.1",
"eslint-plugin-promise": "~6.4.0",
"eslint-plugin-react": "~7.34.3",
"eslint-plugin-react-hooks": "~4.6.2",
"eslint-plugin-unicorn": "~53.0.0",
"eslint-plugin-unicorn": "~54.0.0",
"eslint-plugin-you-dont-need-lodash-underscore": "~6.14.0",
"husky": "~8.0.3",
"jsdom": "~24.0.0",
"lint-staged": "~15.2.2",
"jsdom": "~24.1.0",
"lint-staged": "~15.2.7",
"node-html-parser": "~6.1.13",
"npm-run-all": "~4.1.5",
"prettier": "~3.2.5",
"prettier": "~3.3.2",
"querystring": "~0.2.1",
"react-test-renderer": "~18.3.1",
"rimraf": "~5.0.7",
"sass": "^1.77.2",
"sass": "^1.77.6",
"sinon": "~18.0.0",
"stylelint": "~16.5.0",
"stylelint": "~16.6.1",
"stylelint-config-prettier": "~9.0.5",
"stylelint-config-standard-scss": "~13.1.0",
"stylelint-prettier": "~5.0.0",
"stylelint-scss": "~6.3.0",
"terser": "~5.31.0",
"stylelint-scss": "~6.3.2",
"terser": "~5.31.1",
"unexpected": "~13.2.1",
"unexpected-react": "~6.0.2",
"unexpected-sinon": "~11.1.0",
"url": "~0.11.3",
"vite": "~5.2.11",
"vite": "~5.3.3",
"vite-plugin-compression": "~0.5.1",
"vite-plugin-generate-file": "~0.1.1",
"vitest": "~1.6.0"
Expand All @@ -123,5 +125,5 @@
"url": "https://github.com/FreeFeed/freefeed-react-client.git"
},
"license": "MIT",
"packageManager": "yarn@4.1.1"
"packageManager": "yarn@4.3.1"
}
144 changes: 144 additions & 0 deletions src/components/autocomplete/autocomplete.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { useEffect, useMemo, useState } from 'react';
import { useEvent } from 'react-use-event-hook';
import { EventEmitter } from '../../services/drafts-events';
import { setReactInputValue } from '../../utils/set-react-input-value';
import style from './autocomplete.module.scss';
import { Selector } from './selector';

// There should be no alphanumeric characters right before the "@" (to exclude
// email-like strings)
const defaultAnchor = /(^|[^a-z\d])@/gi;

export function Autocomplete({ inputRef, context, anchor = defaultAnchor }) {
const [query, setQuery] = useState(/** @type {string|null}*/ null);

const events = useMemo(() => new EventEmitter(), []);

const keyHandler = useEvent((/** @type {KeyboardEvent}*/ e) => {
if (
query !== null &&
(e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === 'Tab')
) {
e.preventDefault();
e.stopPropagation();
events.emit(e.key);
} else if (e.key === 'Escape') {
setQuery(null);
}
});

useEffect(() => {
const input = inputRef.current;
if (!input) {
return;
}

const inputHandler = (/** @type {Event} */ e) => {
if (e.type === 'selectionchange' && document.activeElement !== input) {
return;
}
const matchPos = getQueryPosition(input, anchor);
setQuery(matchPos ? input.value.slice(matchPos[0], matchPos[1]) : null);
};

// Clears the query after 100ms of no focus. This delay allows to click on
// the selector by mouse.
let timer = 0;
const focusHandler = () => clearTimeout(timer);
const blurHandler = () => (timer = setTimeout(() => setQuery(null), 500));

input.addEventListener('blur', blurHandler);
input.addEventListener('focus', focusHandler);
input.addEventListener('input', inputHandler);
document.addEventListener('selectionchange', inputHandler); // For the caret movements

// Use capture for early "Enter" interception
input.addEventListener('keydown', keyHandler, { capture: true });

return () => {
clearTimeout(timer);
input.removeEventListener('blur', blurHandler);
input.removeEventListener('focus', focusHandler);
input.removeEventListener('input', inputHandler);
document.removeEventListener('selectionchange', inputHandler);
input.removeEventListener('keydown', keyHandler, { capture: true });
};
}, [anchor, inputRef, keyHandler]);

const onSelectHandler = useEvent((text) => replaceQuery(inputRef.current, text, anchor));

if (query) {
return (
<div className={style.wrapper}>
<Selector query={query} events={events} onSelect={onSelectHandler} context={context} />
</div>
);
}

return null;
}

/**
* Extract the potential username from the closest "@" symbol before the caret.
* Returns null if the caret is not in the right place and the query position
* (start, end offsets) if it is.
*
* The algorithm is the following ("|" symbol means the caret position):
*
* - "|@foo bar" => null
* - "@|foo bar" => "foo" ([1,4])
* - "@f|oo bar" => "foo"
* - "@foo| bar" => "foo"
* - "@foo |bar" => null
*
* @param {HTMLInputElement|HTMLTextAreaElement} input
* @returns {[number, number]|null}
*/
function getQueryPosition({ value, selectionStart }, anchor) {
if (!selectionStart) {
return null;
}

anchor.lastIndex = 0;

let found = -1;
while (anchor.exec(value) !== null) {
if (anchor.lastIndex > selectionStart) {
break;
}
found = anchor.lastIndex;
}

if (found === -1) {
return null;
}

const match = value.slice(found).match(/^[a-z\d-]+/i)?.[0];
// Check that the caret is inside the match or is at its edge
if (!match || match.length <= selectionStart - found - 1) {
return null;
}

return [found, found + match.length];
}

/**
*
* @param {HTMLInputElement|HTMLTextAreaElement} input
* @param {string} replacement
* @returns {void}
*/
function replaceQuery(input, replacement, anchor) {
const matchPos = getQueryPosition(input, anchor);
if (!matchPos) {
return;
}

const before = input.value.slice(0, matchPos[0]);
const after = input.value.slice(matchPos[1]);
const newValue = before + replacement + (after || ' ');
const newCaretPos = matchPos[0] + replacement.length + 1;
setReactInputValue(input, newValue);
input.focus();
input.setSelectionRange(newCaretPos, newCaretPos);
}
76 changes: 76 additions & 0 deletions src/components/autocomplete/autocomplete.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
@import '../../../styles/helvetica/dark-vars.scss';

.wrapper {
position: relative;
}

.selector {
position: absolute;
top: -3px;
left: 0;
width: 100%;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 3px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
z-index: 1;
transition:
opacity 0.2s,
translate 0.2s;

@starting-style {
opacity: 0;
translate: 0 -1em;
}

:global(.dark-theme) & {
background-color: $bg-color-lighter;
border: 1px solid $bg-color-lightest;
}
}

.selector mark {
padding: 0;
font-weight: bold;
background-color: transparent;

:global(.dark-theme) & {
color: $text-color-lighter;
}
}

.list {
list-style: none;
padding: 0;
margin: 0;
}

.item {
padding: 0.5em;
cursor: pointer;
display: flex;
gap: 0.5em;
}

.itemCurrent,
.item:hover {
background-color: #eee;

:global(.dark-theme) & {
background-color: $bg-highlight-color;
}
}

.itemImage {
flex: none;
}

.screenName {
margin-left: 1em;
color: #999;
}

.groupIcon {
color: #aaa;
margin-right: 0.4em;
}
Loading

0 comments on commit 052015c

Please sign in to comment.