Skip to content

Commit a444d49

Browse files
committed
Merge branch 'release' into beta
# Conflicts: # src/components/footer.jsx
2 parents 38f2e69 + 76325cd commit a444d49

20 files changed

+1920
-1292
lines changed

.yarn/releases/yarn-4.9.4.cjs renamed to .yarn/releases/yarn-4.11.0.cjs

Lines changed: 331 additions & 331 deletions
Large diffs are not rendered by default.

.yarnrc.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
nodeLinker: node-modules
22

3-
yarnPath: .yarn/releases/yarn-4.9.4.cjs
3+
yarnPath: .yarn/releases/yarn-4.11.0.cjs

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.142.0] - 2025-11-30
9+
### Added
10+
- Show found users and groups on the search results page.
11+
- Show previews of media links in comments (disabled by default). Supported link
12+
types: FreeFeed images and videos, external images, YouTube videos and shorts.
13+
814
## [1.141.3] - 2025-10-28
915
### Fixed
1016
- Preserve query parameters and hash when initializing browser history.

config/default.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export default {
7171
highlightComments: true,
7272
showTimestamps: false,
7373
hideRepliesToBanned: false,
74+
showMediaPreviews: false,
7475
},
7576
allowLinksPreview: false,
7677
readMoreStyle: 'modern',

package.json

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "reactive-pepyatka",
3-
"version": "1.141.3",
3+
"version": "1.142.0",
44
"description": "",
55
"main": "index.js",
66
"dependencies": {
@@ -23,15 +23,15 @@
2323
"keycode-js": "~3.1.0",
2424
"local-storage-fallback": "~5.0.0",
2525
"lodash-es": "~4.17.21",
26-
"lru-cache": "~11.2.1",
26+
"lru-cache": "~11.2.2",
2727
"memoize-one": "~6.0.0",
2828
"mousetrap": "~1.6.5",
2929
"photoswipe": "~5.4.4",
3030
"photoswipe-video-plugin": "~1.0.2",
3131
"porter-stemmer": "~0.9.1",
3232
"prop-types": "~15.8.1",
33-
"react": "~19.1.1",
34-
"react-dom": "~19.1.1",
33+
"react": "~19.2.0",
34+
"react-dom": "~19.2.0",
3535
"react-final-form-hooks": "~2.0.2",
3636
"react-google-recaptcha": "~3.1.0",
3737
"react-helmet": "~6.1.0",
@@ -41,31 +41,31 @@
4141
"react-sortablejs": "~6.1.4",
4242
"react-textarea-autosize": "~8.5.9",
4343
"react-use-event-hook": "~0.9.6",
44-
"recharts": "~3.2.1",
44+
"recharts": "~3.4.1",
4545
"redux": "~5.0.1",
4646
"regexparam": "~3.0.0",
4747
"snarkdown": "~2.0.0",
4848
"social-text-tokenizer": "~3.2.2",
4949
"socket.io-client": "~2.3.1",
5050
"sortablejs": "~1.15.6",
51-
"tabbable": "~6.2.0",
52-
"ua-parser-js": "~2.0.5",
53-
"use-subscription": "~1.11.0",
54-
"validator": "~13.15.15",
51+
"tabbable": "~6.3.0",
52+
"ua-parser-js": "~2.0.6",
53+
"use-subscription": "~1.12.0",
54+
"validator": "~13.15.23",
5555
"vazirmatn": "^33.0.3"
5656
},
5757
"devDependencies": {
5858
"@eslint/compat": "~1.3.2",
59-
"@eslint/js": "~9.36.0",
59+
"@eslint/js": "~9.39.1",
6060
"@testing-library/dom": "~10.4.1",
61-
"@testing-library/jest-dom": "~6.8.0",
61+
"@testing-library/jest-dom": "~6.9.1",
6262
"@testing-library/react": "~16.3.0",
6363
"@testing-library/user-event": "~14.6.1",
6464
"@vitejs/plugin-legacy": "~7.2.1",
65-
"@vitejs/plugin-react-swc": "^4.1.0",
66-
"cross-env": "~10.0.0",
67-
"esbuild": "~0.25.10",
68-
"eslint": "~9.36.0",
65+
"@vitejs/plugin-react-swc": "^4.2.2",
66+
"cross-env": "~10.1.0",
67+
"esbuild": "~0.27.0",
68+
"eslint": "~9.39.1",
6969
"eslint-config-prettier": "~10.1.8",
7070
"eslint-plugin-import-x": "~4.16.1",
7171
"eslint-plugin-prettier": "~5.5.4",
@@ -75,31 +75,31 @@
7575
"eslint-plugin-unicorn": "~61.0.2",
7676
"eslint-plugin-you-dont-need-lodash-underscore": "~6.14.0",
7777
"glob": "~11.0.3",
78-
"globals": "~16.4.0",
78+
"globals": "~16.5.0",
7979
"gray-matter": "~4.0.3",
8080
"husky": "~9.1.7",
81-
"jsdom": "~27.0.0",
82-
"lint-staged": "~16.2.0",
81+
"jsdom": "~27.2.0",
82+
"lint-staged": "~16.2.6",
8383
"node-html-parser": "~7.0.1",
8484
"npm-run-all": "~4.1.5",
8585
"prettier": "~3.6.2",
8686
"querystring": "~0.2.1",
8787
"remarkable": "~2.0.1",
88-
"rimraf": "~6.0.1",
89-
"sass": "^1.93.0",
88+
"rimraf": "~6.1.0",
89+
"sass": "^1.94.0",
9090
"sinon": "~21.0.0",
91-
"stylelint": "~16.24.0",
91+
"stylelint": "~16.25.0",
9292
"stylelint-config-prettier": "~9.0.5",
9393
"stylelint-config-standard-scss": "~15.0.1",
9494
"stylelint-prettier": "~5.0.3",
9595
"stylelint-scss": "~6.12.1",
96-
"terser": "~5.44.0",
96+
"terser": "~5.44.1",
9797
"unexpected": "~13.2.1",
9898
"unexpected-sinon": "~11.1.0",
9999
"url": "~0.11.4",
100-
"vite": "~7.1.7",
100+
"vite": "~7.2.2",
101101
"vite-plugin-generate-file": "~0.3.1",
102-
"vite-plugin-pwa": "^1.0.3",
102+
"vite-plugin-pwa": "^1.1.0",
103103
"vitest": "~3.2.4",
104104
"workbox-window": "^7.3.0"
105105
},
@@ -123,5 +123,5 @@
123123
"url": "https://github.com/FreeFeed/freefeed-react-client.git"
124124
},
125125
"license": "MIT",
126-
"packageManager": "yarn@4.9.4"
126+
"packageManager": "yarn@4.11.0"
127127
}

src/components/footer.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default function Footer({ short }) {
77
return (
88
<footer className="footer">
99
<p role="navigation">
10-
&copy; FreeFeed 1.141.3-beta (Oct 28, 2025)
10+
&copy; FreeFeed 1.142.0-beta (Nov 30, 2025)
1111
<br />
1212
<Link to="/about">About</Link>
1313
{' | '}

src/components/link-preview/video.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export function getVideoType(url) {
174174
return null;
175175
}
176176

177-
function getVideoId(url) {
177+
export function getVideoId(url) {
178178
let m;
179179
if ((m = YOUTUBE_VIDEO_RE.exec(url))) {
180180
return m[1];
@@ -200,7 +200,7 @@ function getVideoId(url) {
200200
return null;
201201
}
202202

203-
function getDefaultAspectRatio(url) {
203+
export function getDefaultAspectRatio(url) {
204204
if (YOUTUBE_VIDEO_RE.test(url)) {
205205
return isYoutubeShort(url) ? 16 / 9 : 9 / 16;
206206
}

src/components/media-links/helpers.jsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { pauseVimeoVideo, playVimeoVideo } from './vimeo-api';
1616

1717
export const mediaLinksContext = createContext([]);
1818

19-
export function useMediaLink(url) {
19+
export function useMediaLink(url, { previewId } = {}) {
2020
const items = useContext(mediaLinksContext);
2121
const [mediaType, setMediaType] = useState(() => getMediaType(url));
2222
const index = useMemo(() => {
@@ -29,14 +29,17 @@ export function useMediaLink(url) {
2929
} else if (item.mediaType) {
3030
setMediaType(item.mediaType);
3131
}
32+
if (previewId) {
33+
item.pid = previewId;
34+
}
3235
items[index] = item;
3336
return null;
3437
})
3538
.catch((err) => (items[index] = createErrorItem(err)));
3639
return index;
3740
// Items are reference-immutable, url is truly immutable
3841
// eslint-disable-next-line react-hooks/exhaustive-deps
39-
}, []);
42+
}, [previewId]);
4043

4144
const handleClick = useEvent((e) => {
4245
if (!mediaType || !isLeftClick(e)) {
@@ -105,7 +108,7 @@ export function createErrorItem(error) {
105108

106109
const freefeedPathRegex = /^\/attachments\/(?:\w+\/)?([\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12})/;
107110

108-
function freefeedAttachmentId(url) {
111+
export function freefeedAttachmentId(url) {
109112
try {
110113
const urlObj = new URL(url);
111114
if (!CONFIG.attachmentDomains.includes(urlObj.hostname)) {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useEffect, useId, useState } from 'react';
2+
import { freefeedAttachmentId, IMAGE, useMediaLink, VIDEO } from './helpers';
3+
import { getAttachmentInfo } from '../../services/batch-attachments-info';
4+
import { attachmentPreviewUrl } from '../../services/api';
5+
import { getDefaultAspectRatio, getVideoId, T_YOUTUBE_VIDEO } from '../link-preview/video';
6+
import styles from './media-link-preview.module.scss';
7+
8+
export function MediaLinkPreview({ href: url }) {
9+
const previewId = useId();
10+
const [mediaType, handleClick] = useMediaLink(url, { previewId });
11+
12+
if (mediaType !== IMAGE && mediaType !== VIDEO && mediaType !== T_YOUTUBE_VIDEO) {
13+
return null;
14+
}
15+
16+
// Freefeed attachment?
17+
const attId = freefeedAttachmentId(url);
18+
if (attId) {
19+
return (
20+
<a href={url} target="_blank" rel="noreferrer" onClick={handleClick}>
21+
<FreeFeedMediaPreview id={attId} previewId={previewId} />
22+
</a>
23+
);
24+
}
25+
26+
if (mediaType === T_YOUTUBE_VIDEO) {
27+
return (
28+
<a href={url} target="_blank" rel="noreferrer" onClick={handleClick}>
29+
<YouTubeMediaPreview url={url} previewId={previewId} />
30+
</a>
31+
);
32+
}
33+
34+
if (mediaType === VIDEO) {
35+
return null;
36+
}
37+
38+
return (
39+
<a href={url} target="_blank" rel="noreferrer" onClick={handleClick}>
40+
<img
41+
src={url}
42+
alt=""
43+
width={60}
44+
height={60}
45+
className={styles.preview}
46+
loading="lazy"
47+
id={previewId}
48+
/>
49+
</a>
50+
);
51+
}
52+
53+
function FreeFeedMediaPreview({ id, previewId }) {
54+
const [attrs, setAttrs] = useState(null);
55+
56+
useEffect(() => {
57+
getAttachmentInfo(id)
58+
.then((info) => {
59+
if (info?.mediaType === 'image' || info?.mediaType === 'video') {
60+
const height = 120;
61+
const width = Math.round((info.width / info.height) * height);
62+
setAttrs({
63+
src: attachmentPreviewUrl(id, 'image', width, height),
64+
width,
65+
height,
66+
});
67+
}
68+
return null;
69+
})
70+
.catch((err) => {
71+
// Just ignore
72+
// eslint-disable-next-line no-console
73+
console.error('Failed to get attachment info', id, err);
74+
});
75+
}, [id]);
76+
77+
return attrs ? (
78+
<img
79+
src={attrs.src}
80+
alt=""
81+
width={attrs.width}
82+
height={attrs.height}
83+
style={{ '--ar': attrs.width / attrs.height }}
84+
className={styles.preview}
85+
loading="lazy"
86+
id={previewId}
87+
/>
88+
) : null;
89+
}
90+
91+
function YouTubeMediaPreview({ url, previewId }) {
92+
const aspectRatio = getDefaultAspectRatio(url);
93+
return (
94+
<img
95+
src={`https://img.youtube.com/vi/${getVideoId(url)}/default.jpg`}
96+
alt=""
97+
width={Math.round(60 / aspectRatio)}
98+
height={60}
99+
style={{ '--ar': 1 / aspectRatio }}
100+
className={styles.preview}
101+
loading="lazy"
102+
id={previewId}
103+
/>
104+
);
105+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.preview {
2+
height: 4em;
3+
width: auto;
4+
aspect-ratio: clamp(0.8, var(--ar, 1), 1.2);
5+
object-fit: cover;
6+
box-shadow: 0 0 0 1px rgb(0, 0, 0, 0.2);
7+
8+
&:hover {
9+
box-shadow: 0 0 0 1px rgb(0, 0, 0, 1);
10+
}
11+
12+
:global(.dark-theme) & {
13+
box-shadow: 0 0 0 1px rgb(255, 255, 255, 0.2);
14+
15+
&:hover {
16+
box-shadow: 0 0 0 1px rgb(255, 255, 255, 0.8);
17+
}
18+
}
19+
}

0 commit comments

Comments
 (0)