Merge branch 'main' into fix-nsfw-blur-spill

This commit is contained in:
Dessalines 2023-06-15 07:55:09 -04:00 committed by GitHub
commit 1088ee293b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 208 additions and 101 deletions

View file

@ -9,6 +9,19 @@ body:
Found a bug? Please fill out the sections below. 👍 Found a bug? Please fill out the sections below. 👍
Thanks for taking the time to fill out this bug report! Thanks for taking the time to fill out this bug report!
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy) For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
- type: checkboxes
attributes:
label: Requirements
description: Before you create a bug report please do the following.
options:
- label: Is this a bug report? For questions or discussions use https://lemmy.ml/c/lemmy_support
required: true
- label: Did you check to see if this issue already exists?
required: true
- label: Is this only a single bug? Do not put multiple bugs in one issue.
required: true
- label: Is this a server side (not related to the UI) issue? Use the [Lemmy back end](https://github.com/LemmyNet/lemmy) repo.
required: true
- type: textarea - type: textarea
id: summary id: summary
attributes: attributes:
@ -45,3 +58,9 @@ body:
placeholder: ex. 0.17.4-rc.4 placeholder: ex. 0.17.4-rc.4
validations: validations:
required: true required: true
- type: input
id: lemmy-instance
attributes:
label: Lemmy Instance URL
description: Which Lemmy instance do you use? The address
placeholder: lemmy.ml, lemmy.world, etc

View file

@ -7,6 +7,19 @@ body:
value: | value: |
Have a suggestion about Lemmy's UI? Have a suggestion about Lemmy's UI?
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy) For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
- type: checkboxes
attributes:
label: Requirements
description: Before you create a bug report please do the following.
options:
- label: Is this a feature request? For questions or discussions use https://lemmy.ml/c/lemmy_support
required: true
- label: Did you check to see if this issue already exists?
required: true
- label: Is this only a feature request? Do not put multiple feature requests in one issue.
required: true
- label: Is this a server side (not related to the UI) issue? Use the [Lemmy back end](https://github.com/LemmyNet/lemmy) repo.
required: true
- type: textarea - type: textarea
id: problem id: problem
attributes: attributes:

View file

@ -1,6 +1,6 @@
{ {
"name": "lemmy-ui", "name": "lemmy-ui",
"version": "0.18.0-beta.6", "version": "0.18.0-rc.1",
"description": "An isomorphic UI for lemmy", "description": "An isomorphic UI for lemmy",
"repository": "https://github.com/LemmyNet/lemmy-ui", "repository": "https://github.com/LemmyNet/lemmy-ui",
"license": "AGPL-3.0", "license": "AGPL-3.0",

View file

@ -75,6 +75,11 @@
font-size: 1.2rem; font-size: 1.2rem;
} }
.md-div pre {
white-space: pre;
overflow-x: auto;
}
.md-div table { .md-div table {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
@ -213,6 +218,11 @@ blockquote {
overflow-y: auto; overflow-y: auto;
} }
.comments {
list-style: none;
padding: 0;
}
.thumbnail { .thumbnail {
object-fit: cover; object-fit: cover;
min-height: 60px; min-height: 60px;

View file

@ -156,7 +156,7 @@ server.get("/*", async (req, res) => {
site = try_site.data; site = try_site.data;
initializeSite(site); initializeSite(site);
if (path != "/setup" && !site.site_view.local_site.site_setup) { if (path !== "/setup" && !site.site_view.local_site.site_setup) {
return res.redirect("/setup"); return res.redirect("/setup");
} }

View file

@ -16,8 +16,10 @@ import {
isBrowser, isBrowser,
myAuth, myAuth,
numToSI, numToSI,
poll,
showAvatars, showAvatars,
toast, toast,
updateUnreadCountsInterval,
} from "../../utils"; } from "../../utils";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
import { PictrsImage } from "../common/pictrs-image"; import { PictrsImage } from "../common/pictrs-image";
@ -64,7 +66,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
if (isBrowser()) { if (isBrowser()) {
// On the first load, check the unreads // On the first load, check the unreads
this.requestNotificationPermission(); this.requestNotificationPermission();
await this.fetchUnreads(); this.fetchUnreads();
this.requestNotificationPermission(); this.requestNotificationPermission();
document.addEventListener("mouseup", this.handleOutsideMenuClick); document.addEventListener("mouseup", this.handleOutsideMenuClick);
@ -406,10 +408,11 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
return amAdmin() || moderatesS; return amAdmin() || moderatesS;
} }
async fetchUnreads() { fetchUnreads() {
poll(async () => {
if (window.document.visibilityState !== "hidden") {
const auth = myAuth(); const auth = myAuth();
if (auth) { if (auth) {
this.setState({ unreadInboxCountRes: { state: "loading" } });
this.setState({ this.setState({
unreadInboxCountRes: await HttpService.client.getUnreadCount({ unreadInboxCountRes: await HttpService.client.getUnreadCount({
auth, auth,
@ -417,7 +420,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
}); });
if (this.moderatesSomething) { if (this.moderatesSomething) {
this.setState({ unreadReportCountRes: { state: "loading" } });
this.setState({ this.setState({
unreadReportCountRes: await HttpService.client.getReportCount({ unreadReportCountRes: await HttpService.client.getReportCount({
auth, auth,
@ -426,7 +428,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
} }
if (amAdmin()) { if (amAdmin()) {
this.setState({ unreadApplicationCountRes: { state: "loading" } });
this.setState({ this.setState({
unreadApplicationCountRes: unreadApplicationCountRes:
await HttpService.client.getUnreadRegistrationApplicationCount({ await HttpService.client.getUnreadRegistrationApplicationCount({
@ -436,6 +437,8 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
} }
} }
} }
}, updateUnreadCountsInterval);
}
get unreadInboxCount(): number { get unreadInboxCount(): number {
if (this.state.unreadInboxCountRes.state == "success") { if (this.state.unreadInboxCountRes.state == "success") {

View file

@ -270,9 +270,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
this.props.moderators this.props.moderators
); );
const borderColor = this.props.node.depth
? colorList[(this.props.node.depth - 1) % colorList.length]
: colorList[0];
const moreRepliesBorderColor = this.props.node.depth const moreRepliesBorderColor = this.props.node.depth
? colorList[this.props.node.depth % colorList.length] ? colorList[this.props.node.depth % colorList.length]
: colorList[0]; : colorList[0];
@ -284,26 +281,17 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
node.comment_view.counts.child_count > 0; node.comment_view.counts.child_count > 0;
return ( return (
<div <li className="comment" role="comment">
className={`comment ${
this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
}`}
>
<div <div
id={`comment-${cv.comment.id}`} id={`comment-${cv.comment.id}`}
className={classNames(`details comment-node py-2`, { className={classNames(`details comment-node py-2`, {
"border-top border-light": !this.props.noBorder, "border-top border-light": !this.props.noBorder,
mark: this.isCommentNew || this.commentView.comment.distinguished, mark: this.isCommentNew || this.commentView.comment.distinguished,
})} })}
style={
!this.props.noIndent && this.props.node.depth
? `border-left: 2px ${borderColor} solid !important`
: ""
}
> >
<div <div
className={classNames({ className={classNames({
"ml-2": !this.props.noIndent && this.props.node.depth, "ml-2": !this.props.noIndent,
})} })}
> >
<div className="d-flex flex-wrap align-items-center text-muted small"> <div className="d-flex flex-wrap align-items-center text-muted small">
@ -959,9 +947,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</div> </div>
{showMoreChildren && ( {showMoreChildren && (
<div <div
className={`details ml-1 comment-node py-2 ${ className={classNames("details ml-1 comment-node py-2", {
!this.props.noBorder ? "border-top border-light" : "" "border-top border-light": !this.props.noBorder,
}`} })}
style={`border-left: 2px ${moreRepliesBorderColor} solid !important`} style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
> >
<button <button
@ -1169,6 +1157,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages} siteLanguages={this.props.siteLanguages}
hideImages={this.props.hideImages} hideImages={this.props.hideImages}
isChild={!this.props.noIndent}
depth={this.props.node.depth + 1}
finished={this.props.finished} finished={this.props.finished}
onCommentReplyRead={this.props.onCommentReplyRead} onCommentReplyRead={this.props.onCommentReplyRead}
onPersonMentionRead={this.props.onPersonMentionRead} onPersonMentionRead={this.props.onPersonMentionRead}
@ -1192,8 +1182,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
/> />
)} )}
{/* A collapsed clearfix */} {/* A collapsed clearfix */}
{this.state.collapsed && <div className="row col-12"></div>} {this.state.collapsed && <div className="row col-12" />}
</div> </li>
); );
} }
@ -1211,6 +1201,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
linkBtn(small = false) { linkBtn(small = false) {
const cv = this.commentView; const cv = this.commentView;
const classnames = classNames("btn btn-link btn-animate text-muted", { const classnames = classNames("btn btn-link btn-animate text-muted", {
"btn-sm": small, "btn-sm": small,
}); });

View file

@ -1,3 +1,4 @@
import classNames from "classnames";
import { Component } from "inferno"; import { Component } from "inferno";
import { import {
AddAdmin, AddAdmin,
@ -25,6 +26,7 @@ import {
TransferCommunity, TransferCommunity,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { CommentNodeI, CommentViewType } from "../../interfaces"; import { CommentNodeI, CommentViewType } from "../../interfaces";
import { colorList } from "../../utils";
import { CommentNode } from "./comment-node"; import { CommentNode } from "./comment-node";
interface CommentNodesProps { interface CommentNodesProps {
@ -44,6 +46,8 @@ interface CommentNodesProps {
allLanguages: Language[]; allLanguages: Language[];
siteLanguages: number[]; siteLanguages: number[];
hideImages?: boolean; hideImages?: boolean;
isChild?: boolean;
depth?: number;
finished: Map<CommentId, boolean | undefined>; finished: Map<CommentId, boolean | undefined>;
onSaveComment(form: SaveComment): void; onSaveComment(form: SaveComment): void;
onCommentReplyRead(form: MarkCommentReplyAsRead): void; onCommentReplyRead(form: MarkCommentReplyAsRead): void;
@ -74,8 +78,19 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
render() { render() {
const maxComments = this.props.maxCommentsShown ?? this.props.nodes.length; const maxComments = this.props.maxCommentsShown ?? this.props.nodes.length;
const borderColor = this.props.depth
? colorList[this.props.depth % colorList.length]
: colorList[0];
return ( return (
<div className="comments"> this.props.nodes.length > 0 && (
<ul
className={classNames("comments", {
"ms-1": !!this.props.isChild,
"border-top border-light": !this.props.noBorder,
})}
style={`border-left: 2px solid ${borderColor} !important;`}
>
{this.props.nodes.slice(0, maxComments).map(node => ( {this.props.nodes.slice(0, maxComments).map(node => (
<CommentNode <CommentNode
key={node.comment_view.comment.id} key={node.comment_view.comment.id}
@ -116,7 +131,8 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
onPurgeComment={this.props.onPurgeComment} onPurgeComment={this.props.onPurgeComment}
/> />
))} ))}
</div> </ul>
)
); );
} }
} }

View file

@ -195,7 +195,7 @@ export class Login extends Component<any, State> {
} }
handleLoginUsernameChange(i: Login, event: any) { handleLoginUsernameChange(i: Login, event: any) {
i.state.form.username_or_email = event.target.value; i.state.form.username_or_email = event.target.value.trim();
i.setState(i.state); i.setState(i.state);
} }

View file

@ -221,7 +221,7 @@ export class Setup extends Component<any, State> {
} }
handleRegisterUsernameChange(i: Setup, event: any) { handleRegisterUsernameChange(i: Setup, event: any) {
i.state.form.username = event.target.value; i.state.form.username = event.target.value.trim();
i.setState(i.state); i.setState(i.state);
} }

View file

@ -496,7 +496,7 @@ export class Signup extends Component<any, State> {
} }
handleRegisterUsernameChange(i: Signup, event: any) { handleRegisterUsernameChange(i: Signup, event: any) {
i.state.form.username = event.target.value; i.state.form.username = event.target.value.trim();
i.setState(i.state); i.setState(i.state);
} }

View file

@ -25,7 +25,6 @@ import {
isImage, isImage,
myAuth, myAuth,
myAuthRequired, myAuthRequired,
pictrsDeleteToast,
relTags, relTags,
setupTippy, setupTippy,
toast, toast,
@ -73,6 +72,7 @@ interface PostFormState {
suggestedPostsRes: RequestState<SearchResponse>; suggestedPostsRes: RequestState<SearchResponse>;
metadataRes: RequestState<GetSiteMetadataResponse>; metadataRes: RequestState<GetSiteMetadataResponse>;
imageLoading: boolean; imageLoading: boolean;
imageDeleteUrl: string;
communitySearchLoading: boolean; communitySearchLoading: boolean;
communitySearchOptions: Choice[]; communitySearchOptions: Choice[];
previewMode: boolean; previewMode: boolean;
@ -86,6 +86,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
form: {}, form: {},
loading: false, loading: false,
imageLoading: false, imageLoading: false,
imageDeleteUrl: "",
communitySearchLoading: false, communitySearchLoading: false,
previewMode: false, previewMode: false,
communitySearchOptions: [], communitySearchOptions: [],
@ -269,6 +270,17 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
{url && isImage(url) && ( {url && isImage(url) && (
<img src={url} className="img-fluid" alt="" /> <img src={url} className="img-fluid" alt="" />
)} )}
{this.state.imageDeleteUrl && (
<button
className="btn btn-danger btn-sm mt-2"
onClick={linkEvent(this, this.handleImageDelete)}
aria-label={i18n.t("delete")}
data-tippy-content={i18n.t("delete")}
>
<Icon icon="x" classes="icon-inline mr-1" />
{capitalizeFirstLetter(i18n.t("delete"))}
</button>
)}
{this.props.crossPosts && this.props.crossPosts.length > 0 && ( {this.props.crossPosts && this.props.crossPosts.length > 0 && (
<> <>
<div className="my-1 text-muted small font-weight-bold"> <div className="my-1 text-muted small font-weight-bold">
@ -553,7 +565,15 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} }
handlePostUrlChange(i: PostForm, event: any) { handlePostUrlChange(i: PostForm, event: any) {
i.setState(s => ((s.form.url = event.target.value), s)); const url = event.target.value;
i.setState({
form: {
url,
},
imageDeleteUrl: "",
});
i.fetchPageTitle(); i.fetchPageTitle();
} }
@ -644,18 +664,35 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
if (res.state === "success") { if (res.state === "success") {
if (res.data.msg === "ok") { if (res.data.msg === "ok") {
i.state.form.url = res.data.url; i.state.form.url = res.data.url;
pictrsDeleteToast(file.name, res.data.delete_url as string); i.setState({
i.setState({ imageLoading: false }); imageLoading: false,
imageDeleteUrl: res.data.delete_url as string,
});
} else { } else {
toast(JSON.stringify(res), "danger"); toast(JSON.stringify(res), "danger");
} }
} else if (res.state === "failed") { } else if (res.state === "failed") {
console.error(res.msg); console.error(res.msg);
toast(res.msg, "danger"); toast(res.msg, "danger");
i.setState({ imageLoading: false });
} }
}); });
} }
handleImageDelete(i: PostForm) {
const { imageDeleteUrl } = i.state;
fetch(imageDeleteUrl);
i.setState({
imageDeleteUrl: "",
imageLoading: false,
form: {
url: "",
},
});
}
handleCommunitySearch = debounce(async (text: string) => { handleCommunitySearch = debounce(async (text: string) => {
const { selectedCommunityChoice } = this.props; const { selectedCommunityChoice } = this.props;
this.setState({ communitySearchLoading: true }); this.setState({ communitySearchLoading: true });

View file

@ -835,6 +835,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
search: "", search: "",
}} }}
title={i18n.t("cross_post")} title={i18n.t("cross_post")}
data-tippy-content={i18n.t("cross_post")}
aria-label={i18n.t("cross_post")}
> >
<Icon icon="copy" inline /> <Icon icon="copy" inline />
</Link> </Link>

View file

@ -75,6 +75,7 @@ export const commentTreeMaxDepth = 8;
export const markdownFieldCharacterLimit = 50000; export const markdownFieldCharacterLimit = 50000;
export const maxUploadImages = 20; export const maxUploadImages = 20;
export const concurrentImageUpload = 4; export const concurrentImageUpload = 4;
export const updateUnreadCountsInterval = 30000;
export const relTags = "noopener nofollow"; export const relTags = "noopener nofollow";
@ -1127,7 +1128,7 @@ export const colorList: string[] = [
]; ];
function hsl(num: number) { function hsl(num: number) {
return `hsla(${num}, 35%, 50%, 1)`; return `hsla(${num}, 35%, 50%, 0.5)`;
} }
export function hostname(url: string): string { export function hostname(url: string): string {
@ -1490,3 +1491,18 @@ export function newVote(voteType: VoteType, myVote?: number): number {
return myVote == -1 ? 0 : -1; return myVote == -1 ? 0 : -1;
} }
} }
function sleep(millis: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, millis));
}
/**
* Polls / repeatedly runs a promise, every X milliseconds
*/
export async function poll(promiseFn: any, millis: number) {
if (window.document.visibilityState !== "hidden") {
await promiseFn();
}
await sleep(millis);
return poll(promiseFn, millis);
}