Fixing titleOnly, PostSort, and CommentSort. (#2715)

* Fixing titleOnly, PostSort, and CommentSort.

* SortType, Tagline, Emojis (#2718)

* PostSortType

* Hide post sort types in comment view

* Tagline

* CustomEmoji

* Update lemmy-js-client to 0.20.0-alpha.17

* Prompt before leaving unsaved forms

* Add cancel buttons, only create taglines when saving

* Cleanup SortSelect

* Use markdown url for custom emojis

This prevent SSR and CSR from rendering different images after changing
the image of an emoji, already posted emojis will keep showing the old
image. This will also display the same image on different instances that
have overlapping custom emojis.

* Cleanup EmojisForm sorting

* Use existing CommentSortSelect

* Simpler sort type conversion

---------

Co-authored-by: matc-pub <161147791+matc-pub@users.noreply.github.com>
This commit is contained in:
Dessalines 2024-10-01 14:40:02 -04:00 committed by GitHub
parent 6057c96f0c
commit 6b5da8cfb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 815 additions and 580 deletions

View File

@ -60,7 +60,7 @@
"inferno-router": "^8.2.3",
"inferno-server": "^8.2.3",
"jwt-decode": "^4.0.0",
"lemmy-js-client": "0.20.0-alpha.7",
"lemmy-js-client": "0.20.0-alpha.17",
"lodash.isequal": "^4.5.0",
"markdown-it": "^14.1.0",
"markdown-it-bidi": "^0.2.0",

View File

@ -114,8 +114,8 @@ importers:
specifier: ^4.0.0
version: 4.0.0
lemmy-js-client:
specifier: 0.20.0-alpha.7
version: 0.20.0-alpha.7
specifier: 0.20.0-alpha.17
version: 0.20.0-alpha.17
lodash.isequal:
specifier: ^4.5.0
version: 4.5.0
@ -3071,8 +3071,8 @@ packages:
leac@0.6.0:
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
lemmy-js-client@0.20.0-alpha.7:
resolution: {integrity: sha512-lhPs8gJFLX0EvlwkkgtTF2F/v22lKjcQ81u7u5CShrO05Dii33fZ5x9SrEJYHD0qw4FIN/jpfKucN9QnndScpA==}
lemmy-js-client@0.20.0-alpha.17:
resolution: {integrity: sha512-4iZQtZNldhioTecSgi1LMR4E3uK5IcQ+EuWg4aAXmciOIHxPXPAHy7qSLuHqbzEiL1QP5G3MFwQnlVf/sJkFaQ==}
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
@ -7811,7 +7811,7 @@ snapshots:
leac@0.6.0: {}
lemmy-js-client@0.20.0-alpha.7: {}
lemmy-js-client@0.20.0-alpha.17: {}
leven@3.1.0: {}

View File

@ -461,3 +461,7 @@ br.big {
.totp-link {
width: fit-content;
}
em-emoji-picker {
width: 100%;
}

View File

@ -5,6 +5,7 @@ import App from "../shared/components/app/app";
import { lazyHighlightjs } from "../shared/lazy-highlightjs";
import { loadUserLanguage } from "../shared/services/I18NextService";
import { verifyDynamicImports } from "../shared/dynamic-imports";
import { setupEmojiDataModel } from "../shared/markdown";
import "bootstrap/js/dist/collapse";
import "bootstrap/js/dist/dropdown";
@ -22,7 +23,7 @@ async function startClient() {
lazyHighlightjs.enableLazyLoading();
await loadUserLanguage();
await Promise.all([loadUserLanguage(), setupEmojiDataModel()]);
const wrapper = (
<BrowserRouter>

View File

@ -47,13 +47,13 @@ export class CommentSortSelect extends Component<
<option disabled aria-hidden="true">
{I18NextService.i18n.t("sort_type")}
</option>
<option value={"Hot"}>{I18NextService.i18n.t("hot")}</option>,
<option value={"Controversial"}>
<option value="Hot">{I18NextService.i18n.t("hot")}</option>
<option value="Controversial">
{I18NextService.i18n.t("controversial")}
</option>
<option value={"Top"}>{I18NextService.i18n.t("top")}</option>,
<option value={"New"}>{I18NextService.i18n.t("new")}</option>
<option value={"Old"}>{I18NextService.i18n.t("old")}</option>
<option value="Top">{I18NextService.i18n.t("top")}</option>
<option value="New">{I18NextService.i18n.t("new")}</option>
<option value="Old">{I18NextService.i18n.t("old")}</option>
</select>
<a
className="sort-select-help text-muted"

View File

@ -5,6 +5,7 @@ interface PaginatorProps {
page: number;
onChange(val: number): any;
nextDisabled: boolean;
disabled?: boolean;
}
export class Paginator extends Component<PaginatorProps, any> {
@ -18,6 +19,7 @@ export class Paginator extends Component<PaginatorProps, any> {
<button
className="btn btn-secondary me-2"
onClick={linkEvent(this, this.handlePrev)}
disabled={this.props.disabled}
>
{I18NextService.i18n.t("prev")}
</button>
@ -26,6 +28,7 @@ export class Paginator extends Component<PaginatorProps, any> {
<button
className="btn btn-secondary"
onClick={linkEvent(this, this.handleNext)}
disabled={this.props.disabled}
>
{I18NextService.i18n.t("next")}
</button>

View File

@ -1,19 +1,19 @@
import { randomStr } from "@utils/helpers";
import { Component, linkEvent } from "inferno";
import { SortType } from "lemmy-js-client";
import { PostSortType } from "lemmy-js-client";
import { relTags, sortingHelpUrl } from "../../config";
import { I18NextService } from "../../services";
import { Icon } from "./icon";
interface SortSelectProps {
sort: SortType;
onChange(val: SortType): void;
sort: PostSortType;
onChange(val: PostSortType): void;
hideHot?: boolean;
hideMostComments?: boolean;
}
interface SortSelectState {
sort: SortType;
sort: PostSortType;
}
export class SortSelect extends Component<SortSelectProps, SortSelectState> {
@ -47,55 +47,53 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
{I18NextService.i18n.t("sort_type")}
</option>
{!this.props.hideHot && [
<option key={"Hot"} value={"Hot"}>
<option key="Hot" value="Hot">
{I18NextService.i18n.t("hot")}
</option>,
<option key={"Active"} value={"Active"}>
<option key="Active" value="Active">
{I18NextService.i18n.t("active")}
</option>,
<option key={"Scaled"} value={"Scaled"}>
<option key="Scaled" value="Scaled">
{I18NextService.i18n.t("scaled")}
</option>,
]}
<option value={"Controversial"}>
<option value="Controversial">
{I18NextService.i18n.t("controversial")}
</option>
<option value={"New"}>{I18NextService.i18n.t("new")}</option>
<option value={"Old"}>{I18NextService.i18n.t("old")}</option>
<option value="New">{I18NextService.i18n.t("new")}</option>
<option value="Old">{I18NextService.i18n.t("old")}</option>
{!this.props.hideMostComments && [
<option key={"MostComments"} value={"MostComments"}>
<option key="MostComments" value="MostComments">
{I18NextService.i18n.t("most_comments")}
</option>,
<option key={"NewComments"} value={"NewComments"}>
<option key="NewComments" value="NewComments">
{I18NextService.i18n.t("new_comments")}
</option>,
]}
<option disabled aria-hidden="true">
</option>
<option value={"TopHour"}>{I18NextService.i18n.t("top_hour")}</option>
<option value={"TopSixHour"}>
<option value="TopHour">{I18NextService.i18n.t("top_hour")}</option>
<option value="TopSixHour">
{I18NextService.i18n.t("top_six_hours")}
</option>
<option value={"TopTwelveHour"}>
<option value="TopTwelveHour">
{I18NextService.i18n.t("top_twelve_hours")}
</option>
<option value={"TopDay"}>{I18NextService.i18n.t("top_day")}</option>
<option value={"TopWeek"}>{I18NextService.i18n.t("top_week")}</option>
<option value={"TopMonth"}>
{I18NextService.i18n.t("top_month")}
</option>
<option value={"TopThreeMonths"}>
<option value="TopDay">{I18NextService.i18n.t("top_day")}</option>
<option value="TopWeek">{I18NextService.i18n.t("top_week")}</option>
<option value="TopMonth">{I18NextService.i18n.t("top_month")}</option>
<option value="TopThreeMonths">
{I18NextService.i18n.t("top_three_months")}
</option>
<option value={"TopSixMonths"}>
<option value="TopSixMonths">
{I18NextService.i18n.t("top_six_months")}
</option>
<option value={"TopNineMonths"}>
<option value="TopNineMonths">
{I18NextService.i18n.t("top_nine_months")}
</option>
<option value={"TopYear"}>{I18NextService.i18n.t("top_year")}</option>
<option value={"TopAll"}>{I18NextService.i18n.t("top_all")}</option>
<option value="TopYear">{I18NextService.i18n.t("top_year")}</option>
<option value="TopAll">{I18NextService.i18n.t("top_all")}</option>
</select>
<a
className="sort-select-icon text-muted"

View File

@ -16,7 +16,7 @@ import {
ListCommunities,
ListCommunitiesResponse,
ListingType,
SortType,
PostSortType,
} from "lemmy-js-client";
import { InitialFetchRequest } from "../../interfaces";
import { FirstLoadService, I18NextService } from "../../services";
@ -55,7 +55,7 @@ interface CommunitiesState {
interface CommunitiesProps {
listingType: ListingType;
sort: SortType;
sort: PostSortType;
page: number;
}
@ -63,8 +63,8 @@ function getListingTypeFromQuery(listingType?: string): ListingType {
return listingType ? (listingType as ListingType) : "Local";
}
function getSortTypeFromQuery(type?: string): SortType {
return type ? (type as SortType) : "TopMonth";
function getSortTypeFromQuery(type?: string): PostSortType {
return type ? (type as PostSortType) : "TopMonth";
}
export function getCommunitiesQueryParams(source?: string): CommunitiesProps {
@ -302,7 +302,7 @@ export class Communities extends Component<
this.updateUrl({ page });
}
handleSortChange(val: SortType) {
handleSortChange(val: PostSortType) {
this.updateUrl({ sort: val, page: 1 });
}

View File

@ -1,5 +1,6 @@
import {
commentsToFlatNodes,
commentToPostSortType,
communityRSSUrl,
editComment,
editPost,
@ -81,9 +82,10 @@ import {
RemovePost,
SaveComment,
SavePost,
SortType,
PostSortType,
SuccessResponse,
TransferCommunity,
CommentSortType,
} from "lemmy-js-client";
import { fetchLimit, relTags } from "../../config";
import {
@ -121,6 +123,7 @@ import { IRoutePropsWithFetch } from "../../routes";
import PostHiddenSelect from "../common/post-hidden-select";
import { isBrowser } from "@utils/browser";
import { LoadingEllipses } from "../common/loading-ellipses";
import { CommentSortSelect } from "../common/comment-sort-select";
type CommunityData = RouteDataResponse<{
communityRes: GetCommunityResponse;
@ -139,12 +142,12 @@ interface State {
interface CommunityProps {
dataType: DataType;
sort: SortType;
sort: PostSortType;
pageCursor?: PaginationCursor;
showHidden?: StringBoolean;
}
type Fallbacks = { sort: SortType };
type Fallbacks = { sort: PostSortType };
export function getCommunityQueryParams(
source: string | undefined,
@ -162,7 +165,8 @@ export function getCommunityQueryParams(
},
source,
{
sort: local_user?.default_sort_type ?? local_site.default_sort_type,
sort:
local_user?.default_post_sort_type ?? local_site.default_post_sort_type,
},
);
}
@ -171,12 +175,11 @@ function getDataTypeFromQuery(type?: string): DataType {
return type ? DataType[type] : DataType.Post;
}
function getSortTypeFromQuery(type?: string): SortType {
const mySortType =
UserService.Instance.myUserInfo?.local_user_view.local_user
.default_sort_type;
return type ? (type as SortType) : (mySortType ?? "Active");
function getSortTypeFromQuery(
type: string | undefined,
fallback: PostSortType,
): PostSortType {
return type ? (type as PostSortType) : fallback;
}
type CommunityPathProps = { name: string };
@ -215,6 +218,7 @@ export class Community extends Component<CommunityRouteProps, State> {
super(props, context);
this.handleSortChange = this.handleSortChange.bind(this);
this.handleCommentSortChange = this.handleCommentSortChange.bind(this);
this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
this.handlePageNext = this.handlePageNext.bind(this);
this.handlePagePrev = this.handlePagePrev.bind(this);
@ -628,7 +632,14 @@ export class Community extends Component<CommunityRouteProps, State> {
</span>
)}
<span className="me-2">
<SortSelect sort={sort} onChange={this.handleSortChange} />
{this.props.dataType === DataType.Post ? (
<SortSelect sort={sort} onChange={this.handleSortChange} />
) : (
<CommentSortSelect
sort={postToCommentSortType(sort)}
onChange={this.handleCommentSortChange}
/>
)}
</span>
{communityRss && (
<>
@ -654,8 +665,18 @@ export class Community extends Component<CommunityRouteProps, State> {
this.updateUrl({ pageCursor: nextPage });
}
handleSortChange(sort: SortType) {
this.updateUrl({ sort, pageCursor: undefined });
handleSortChange(sort: PostSortType) {
this.updateUrl({
sort: sort,
pageCursor: undefined,
});
}
handleCommentSortChange(sort: CommentSortType) {
this.updateUrl({
sort: commentToPostSortType(sort),
pageCursor: undefined,
});
}
handleDataTypeChange(dataType: DataType) {

View File

@ -6,9 +6,6 @@ import classNames from "classnames";
import { Component } from "inferno";
import {
BannedPersonsResponse,
CreateCustomEmoji,
DeleteCustomEmoji,
EditCustomEmoji,
EditSite,
GetFederatedInstancesResponse,
GetSiteResponse,
@ -17,7 +14,6 @@ import {
PersonView,
} from "lemmy-js-client";
import { InitialFetchRequest } from "../../interfaces";
import { removeFromEmojiDataModel, updateEmojiDataModel } from "../../markdown";
import { FirstLoadService, I18NextService } from "../../services";
import {
EMPTY_REQUEST,
@ -104,9 +100,6 @@ export class AdminSettings extends Component<
super(props, context);
this.handleEditSite = this.handleEditSite.bind(this);
this.handleEditEmoji = this.handleEditEmoji.bind(this);
this.handleDeleteEmoji = this.handleDeleteEmoji.bind(this);
this.handleCreateEmoji = this.handleCreateEmoji.bind(this);
this.handleUploadsPageChange = this.handleUploadsPageChange.bind(this);
this.handleToggleShowLeaveAdminConfirmation =
this.handleToggleShowLeaveAdminConfirmation.bind(this);
@ -249,11 +242,7 @@ export class AdminSettings extends Component<
id="taglines-tab-pane"
>
<div className="row">
<TaglineForm
taglines={this.state.siteRes.taglines}
onSaveSite={this.handleEditSite}
loading={this.state.loading}
/>
<TaglineForm />
</div>
</div>
),
@ -270,11 +259,7 @@ export class AdminSettings extends Component<
id="emojis-tab-pane"
>
<div className="row">
<EmojiForm
onCreate={this.handleCreateEmoji}
onDelete={this.handleDeleteEmoji}
onEdit={this.handleEditEmoji}
/>
<EmojiForm />
</div>
</div>
),
@ -431,7 +416,6 @@ export class AdminSettings extends Component<
this.setState(s => {
s.siteRes.site_view = editRes.data.site_view;
// TODO: Where to get taglines from?
s.siteRes.taglines = editRes.data.taglines;
return s;
});
toast(I18NextService.i18n.t("site_saved"));
@ -464,27 +448,6 @@ export class AdminSettings extends Component<
}
}
async handleEditEmoji(form: EditCustomEmoji) {
const res = await HttpService.client.editCustomEmoji(form);
if (res.state === "success") {
updateEmojiDataModel(res.data.custom_emoji);
}
}
async handleDeleteEmoji(form: DeleteCustomEmoji) {
const res = await HttpService.client.deleteCustomEmoji(form);
if (res.state === "success") {
removeFromEmojiDataModel(form.id);
}
}
async handleCreateEmoji(form: CreateCustomEmoji) {
const res = await HttpService.client.createCustomEmoji(form);
if (res.state === "success") {
updateEmojiDataModel(res.data.custom_emoji);
}
}
async handleUploadsPageChange(val: number) {
this.setState({ uploadsPage: val });
snapToTop();

View File

@ -1,79 +1,81 @@
import { setIsoData } from "@utils/app";
import { capitalizeFirstLetter } from "@utils/helpers";
import { Component, linkEvent } from "inferno";
import {
CreateCustomEmoji,
DeleteCustomEmoji,
EditCustomEmoji,
GetSiteResponse,
} from "lemmy-js-client";
import { customEmojisLookup } from "../../markdown";
import { CustomEmojiView } from "lemmy-js-client";
import { emojiMartCategories, EmojiMartCategory } from "../../markdown";
import { HttpService, I18NextService } from "../../services";
import { pictrsDeleteToast, toast } from "../../toast";
import { EmojiMart } from "../common/emoji-mart";
import { Icon, Spinner } from "../common/icon";
import { Paginator } from "../common/paginator";
import { tippyMixin } from "../mixins/tippy-mixin";
import { isBrowser } from "@utils/browser";
import classNames from "classnames";
import { amAdmin } from "@utils/roles";
import { Prompt } from "inferno-router";
interface EmojiFormProps {
onEdit(form: EditCustomEmoji): void;
onCreate(form: CreateCustomEmoji): void;
onDelete(form: DeleteCustomEmoji): void;
interface EditableEmoji {
change?: "update" | "delete" | "create";
emoji: CustomEmojiView;
loading?: boolean;
}
function markForUpdate(editable: EditableEmoji) {
if (editable.change !== "create") {
editable.change = "update";
}
}
interface EmojiFormState {
siteRes: GetSiteResponse;
customEmojis: CustomEmojiViewForm[];
page: number;
}
interface CustomEmojiViewForm {
id: number;
category: string;
shortcode: string;
image_url: string;
alt_text: string;
keywords: string;
changed: boolean;
emojis: EditableEmoji[]; // Emojis for the current page
allEmojis: CustomEmojiView[]; // All emojis for emoji lookup across pages
emojiMartCustom: EmojiMartCategory[];
emojiMartKey: number;
page: number;
loading: boolean;
}
@tippyMixin
export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
private isoData = setIsoData(this.context);
export class EmojiForm extends Component<Record<never, never>, EmojiFormState> {
private itemsPerPage = 15;
private emptyState: EmojiFormState = {
siteRes: this.isoData.site_res,
customEmojis: this.isoData.site_res.custom_emojis.map((x, index) => ({
id: x.custom_emoji.id,
category: x.custom_emoji.category,
shortcode: x.custom_emoji.shortcode,
image_url: x.custom_emoji.image_url,
alt_text: x.custom_emoji.alt_text,
keywords: x.keywords.map(x => x.keyword).join(" "),
changed: false,
page: 1 + Math.floor(index / this.itemsPerPage),
loading: false,
})),
private needsRefetch = true;
state: EmojiFormState = {
emojis: [],
allEmojis: [],
emojiMartCustom: [],
emojiMartKey: 1,
loading: false,
page: 1,
};
state: EmojiFormState;
private scrollRef: any = {};
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.handlePageChange = this.handlePageChange.bind(this);
this.handleEmojiClick = this.handleEmojiClick.bind(this);
}
async componentWillMount() {
if (isBrowser()) {
this.handlePageChange(1);
}
}
hasPendingChanges() {
return this.state.emojis.some(x => x.change);
}
render() {
return (
<div className="home-emojis-form col-12">
<Prompt
message={I18NextService.i18n.t("block_leaving")}
when={this.hasPendingChanges()}
/>
<h1 className="h4 mb-4">{I18NextService.i18n.t("custom_emojis")}</h1>
{customEmojisLookup.size > 0 && (
{this.state.emojiMartCustom.length > 0 && (
<div>
<EmojiMart
key={this.state.emojiMartKey}
onEmojiClick={this.handleEmojiClick}
pickerOptions={this.configurePicker()}
></EmojiMart>
@ -87,6 +89,10 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
<thead className="pointer">
<tr>
<th>{I18NextService.i18n.t("column_emoji")}</th>
<th
className="text-right"
// Upload button
/>
<th className="text-right">
{I18NextService.i18n.t("column_shortcode")}
</th>
@ -102,20 +108,15 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
<th className="text-right d-lg-table-cell">
{I18NextService.i18n.t("column_keywords")}
</th>
<th></th>
<th style="width:121px"></th>
</tr>
</thead>
<tbody>
{this.state.customEmojis
.slice(
Number((this.state.page - 1) * this.itemsPerPage),
Number(
(this.state.page - 1) * this.itemsPerPage +
this.itemsPerPage,
),
)
.map((cv, index) => (
<tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
{this.state.emojis.map((editable: EditableEmoji, index) => {
const cv = editable.emoji.custom_emoji;
return (
<tr key={index}>
<td style="text-align:center;">
{cv.image_url.length > 0 && (
<img
@ -124,7 +125,9 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
alt={cv.alt_text}
/>
)}
{cv.image_url.length === 0 && (
</td>
<td>
{
<label
// TODO: Fix this linting violation
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
@ -150,7 +153,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
)}
/>
</label>
)}
}
</td>
<td className="text-right">
<input
@ -167,6 +170,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
</td>
<td className="text-right">
<input
ref={e => (this.scrollRef[cv.shortcode] = e)}
type="text"
placeholder="Category"
className="form-control"
@ -206,31 +210,54 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
type="text"
placeholder="Keywords"
className="form-control"
value={cv.keywords}
value={editable.emoji.keywords
.map(k => k.keyword)
.join(" ")}
onInput={linkEvent(
{ form: this, index: index },
this.handleEmojiKeywordChange,
)}
/>
</td>
<td
className={classNames("", {
"border-info": editable.change === "update",
"border-danger": editable.change === "delete",
"border-warning": editable.change === "create",
})}
>
{editable.change === "update" && (
<span>
<Icon icon="transfer" />
</span>
)}
{editable.change === "delete" && (
<span>
<Icon icon="trash" />
</span>
)}
{editable.change === "create" && (
<span>
<Icon icon="add" />
</span>
)}
</td>
<td>
<div>
<span title={this.getEditTooltip(cv)}>
<div class="row flex-nowrap g-0">
<span class="col" title={this.getEditTooltip(editable)}>
<button
className={
(this.canEdit(cv)
? "text-success "
: "text-muted ") + "btn btn-link btn-animate"
}
className={classNames("btn btn-link btn-animate", {
"text-success": this.canSave(editable),
})}
onClick={linkEvent(
{ i: this, cv: cv },
this.handleEditEmojiClick,
{ i: this, cv: editable },
this.handleSaveEmojiClick,
)}
data-tippy-content={I18NextService.i18n.t("save")}
aria-label={I18NextService.i18n.t("save")}
disabled={!this.canEdit(cv)}
disabled={!this.canSave(editable)}
>
{cv.loading ? (
{editable.loading ? (
<Spinner />
) : (
capitalizeFirstLetter(
@ -240,14 +267,14 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
</button>
</span>
<button
className="btn btn-link btn-animate text-muted"
className="col btn btn-link btn-animate text-muted"
onClick={linkEvent(
{ i: this, index: index, cv: cv },
{ i: this, index: index, cv: editable },
this.handleDeleteEmojiClick,
)}
data-tippy-content={I18NextService.i18n.t("delete")}
aria-label={I18NextService.i18n.t("delete")}
disabled={cv.loading}
disabled={editable.loading}
title={I18NextService.i18n.t("delete")}
>
<Icon
@ -255,10 +282,28 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
classes="icon-inline text-danger"
/>
</button>
<button
className={classNames(
"col btn btn-link btn-animate",
{
"text-danger": !!editable.change,
},
)}
onClick={linkEvent(
{ i: this, cv: editable },
this.handleCancelEmojiClick,
)}
data-tippy-content={I18NextService.i18n.t("cancel")}
aria-label={I18NextService.i18n.t("cancel")}
disabled={!editable.change}
>
{I18NextService.i18n.t("cancel")}
</button>
</div>
</td>
</tr>
))}
);
})}
</tbody>
</table>
<br />
@ -273,43 +318,91 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
page={this.state.page}
onChange={this.handlePageChange}
nextDisabled={false}
disabled={this.hasPendingChanges()}
/>
</div>
</div>
);
}
canEdit(cv: CustomEmojiViewForm) {
const noEmptyFields =
cv.alt_text.length > 0 &&
cv.category.length > 0 &&
cv.image_url.length > 0 &&
cv.shortcode.length > 0;
const noDuplicateShortCodes =
this.state.customEmojis.filter(
x => x.shortcode === cv.shortcode && x.id !== cv.id,
).length === 0;
return noEmptyFields && noDuplicateShortCodes && !cv.loading && cv.changed;
canSave(cv: EditableEmoji) {
const requiredFields =
cv.emoji.custom_emoji.image_url.length > 0 &&
cv.emoji.custom_emoji.shortcode.length > 0;
return requiredFields && !cv.loading;
}
getEditTooltip(cv: CustomEmojiViewForm) {
if (this.canEdit(cv)) return I18NextService.i18n.t("save");
getEditTooltip(cv: EditableEmoji) {
if (this.canSave(cv)) return I18NextService.i18n.t("save");
else return I18NextService.i18n.t("custom_emoji_save_validation");
}
handlePageChange(page: number) {
this.setState({ page: page });
async handlePageChange(page: number) {
this.setState({ loading: true });
let allEmojis: CustomEmojiView[] = this.state.allEmojis;
let emojiMartCustom: EmojiMartCategory[] = this.state.emojiMartCustom;
let emojiMartKey: number = this.state.emojiMartKey;
if (this.needsRefetch) {
const emojiRes = await HttpService.client.listCustomEmojis({
ignore_page_limits: true,
});
if (emojiRes.state === "success") {
this.needsRefetch = false;
allEmojis = emojiRes.data.custom_emojis;
allEmojis.sort((a, b) => {
const categoryOrder = a.custom_emoji.category.localeCompare(
b.custom_emoji.category,
);
if (categoryOrder === 0) {
return a.custom_emoji.shortcode.localeCompare(
b.custom_emoji.shortcode,
);
}
return categoryOrder;
});
}
emojiMartCustom = emojiMartCategories(allEmojis);
emojiMartKey++;
}
if (allEmojis) {
const startIndex = (page - 1) * this.itemsPerPage;
const emojis = allEmojis
.slice(startIndex, startIndex + this.itemsPerPage)
.map(x => ({ emoji: structuredClone(x) })); // clone for restore after cancel
this.setState({
loading: false,
allEmojis,
emojiMartCustom,
emojiMartKey,
emojis,
page,
});
} else {
this.setState({ loading: false, page });
}
}
handleEmojiClick(e: any) {
const view = customEmojisLookup.get(e.id);
if (view) {
const page = this.state.customEmojis.find(
x => x.id === view.custom_emoji.id,
)?.page;
if (page) {
this.setState({ page: page });
this.scrollRef[view.custom_emoji.shortcode].scrollIntoView();
async handleEmojiClick(e: any) {
const emojiIndex = this.state.allEmojis.findIndex(
x => x.custom_emoji.shortcode === e.id,
);
if (emojiIndex >= 0) {
const { shortcode } = this.state.allEmojis[emojiIndex].custom_emoji;
const page = Math.floor(emojiIndex / this.itemsPerPage) + 1;
if (page !== this.state.page) {
if (
this.hasPendingChanges() &&
!confirm(I18NextService.i18n.t("block_leaving"))
) {
return;
}
await this.handlePageChange(page);
await new Promise(r => setTimeout(r));
}
if (shortcode) {
const categoryInput: HTMLInputElement | undefined =
this.scrollRef[shortcode];
categoryInput?.focus();
}
}
}
@ -318,32 +411,22 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
props: { form: EmojiForm; index: number },
event: any,
) {
const custom_emojis = [...props.form.state.customEmojis];
const pagedIndex =
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
const item = {
...props.form.state.customEmojis[pagedIndex],
category: event.target.value,
changed: true,
};
custom_emojis[Number(pagedIndex)] = item;
props.form.setState({ customEmojis: custom_emojis });
const editable: EditableEmoji = props.form.state.emojis[props.index];
props.form.setState(() => {
markForUpdate(editable);
editable.emoji.custom_emoji.category = event.target.value;
});
}
handleEmojiShortCodeChange(
props: { form: EmojiForm; index: number },
event: any,
) {
const custom_emojis = [...props.form.state.customEmojis];
const pagedIndex =
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
const item = {
...props.form.state.customEmojis[pagedIndex],
shortcode: event.target.value,
changed: true,
};
custom_emojis[Number(pagedIndex)] = item;
props.form.setState({ customEmojis: custom_emojis });
const editable: EditableEmoji = props.form.state.emojis[props.index];
props.form.setState(() => {
markForUpdate(editable);
editable.emoji.custom_emoji.shortcode = event.target.value;
});
}
handleEmojiImageUrlChange(
@ -354,28 +437,11 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
}: { form: EmojiForm; index: number; overrideValue: string | null },
event: any,
) {
form.setState(prevState => {
const custom_emojis = [...form.state.customEmojis];
const pagedIndex = (form.state.page - 1) * form.itemsPerPage + index;
const item = {
...form.state.customEmojis[pagedIndex],
image_url: overrideValue ?? event.target.value,
changed: true,
};
custom_emojis[Number(pagedIndex)] = item;
return {
...prevState,
customEmojis: prevState.customEmojis.map((ce, i) =>
i === pagedIndex
? {
...ce,
image_url: overrideValue ?? event.target.value,
changed: true,
loading: false,
}
: ce,
),
};
const editable: EditableEmoji = form.state.emojis[index];
form.setState(() => {
markForUpdate(editable);
editable.emoji.custom_emoji.image_url =
overrideValue ?? event.target.value;
});
}
@ -383,97 +449,117 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
props: { form: EmojiForm; index: number },
event: any,
) {
const custom_emojis = [...props.form.state.customEmojis];
const pagedIndex =
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
const item = {
...props.form.state.customEmojis[pagedIndex],
alt_text: event.target.value,
changed: true,
};
custom_emojis[Number(pagedIndex)] = item;
props.form.setState({ customEmojis: custom_emojis });
const editable: EditableEmoji = props.form.state.emojis[props.index];
props.form.setState(() => {
markForUpdate(editable);
editable.emoji.custom_emoji.alt_text = event.target.value;
});
}
handleEmojiKeywordChange(
props: { form: EmojiForm; index: number },
event: any,
) {
const custom_emojis = [...props.form.state.customEmojis];
const pagedIndex =
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
const item = {
...props.form.state.customEmojis[pagedIndex],
keywords: event.target.value,
changed: true,
};
custom_emojis[Number(pagedIndex)] = item;
props.form.setState({ customEmojis: custom_emojis });
const editable: EditableEmoji = props.form.state.emojis[props.index];
props.form.setState(() => {
markForUpdate(editable);
editable.emoji.keywords = event.target.value
.split(" ")
.map((x: string) => ({ id: -1, keyword: x }));
});
}
handleDeleteEmojiClick(d: {
i: EmojiForm;
index: number;
cv: CustomEmojiViewForm;
cv: EditableEmoji;
}) {
const pagedIndex = (d.i.state.page - 1) * d.i.itemsPerPage + d.index;
if (d.cv.id !== 0) {
d.i.props.onDelete({
id: d.cv.id,
});
if (d.cv.change === "create") {
// This drops the entry immediately, other deletes have to be saved.
d.i.setState(prev => ({
emojis: prev.emojis.filter(x => x !== d.cv),
}));
} else {
const custom_emojis = [...d.i.state.customEmojis];
custom_emojis.splice(Number(pagedIndex), 1);
d.i.setState({ customEmojis: custom_emojis });
}
}
handleEditEmojiClick(d: { i: EmojiForm; cv: CustomEmojiViewForm }) {
const keywords = d.cv.keywords
.split(" ")
.filter(x => x.length > 0) as string[];
const uniqueKeywords = Array.from(new Set(keywords));
if (d.cv.id !== 0) {
d.i.props.onEdit({
id: d.cv.id,
category: d.cv.category,
image_url: d.cv.image_url,
alt_text: d.cv.alt_text,
keywords: uniqueKeywords,
});
} else {
d.i.props.onCreate({
category: d.cv.category,
shortcode: d.cv.shortcode,
image_url: d.cv.image_url,
alt_text: d.cv.alt_text,
keywords: uniqueKeywords,
d.i.setState(() => {
d.cv.change = "delete";
});
}
}
handleAddEmojiClick(form: EmojiForm, event: any) {
async handleSaveEmojiClick(d: { i: EmojiForm; cv: EditableEmoji }) {
d.i.needsRefetch = true;
const editable = d.cv;
if (editable.change === "update") {
const resp = await HttpService.client.editCustomEmoji({
...editable.emoji.custom_emoji,
keywords: editable.emoji.keywords.map(x => x.keyword),
});
if (resp.state === "success") {
d.i.setState(() => {
editable.emoji = resp.data.custom_emoji;
editable.change = undefined;
});
}
} else if (editable.change === "delete") {
const resp = await HttpService.client.deleteCustomEmoji(
editable.emoji.custom_emoji,
);
if (resp.state === "success") {
d.i.setState(prev => ({
emojis: prev.emojis.filter(x => x !== editable),
}));
}
} else if (editable.change === "create") {
const resp = await HttpService.client.createCustomEmoji({
...editable.emoji.custom_emoji,
keywords: editable.emoji.keywords.map(x => x.keyword),
});
if (resp.state === "success") {
d.i.setState(() => {
editable.emoji = resp.data.custom_emoji;
editable.change = undefined;
});
}
}
}
async handleCancelEmojiClick(d: { i: EmojiForm; cv: EditableEmoji }) {
if (d.cv.change === "create") {
d.i.setState(() => {
return {
emojis: d.i.state.emojis.filter(x => x !== d.cv),
};
});
} else if (d.cv.change === "update" || d.cv.change === "delete") {
const original = d.i.state.allEmojis.find(
x => x.custom_emoji.id === d.cv.emoji.custom_emoji.id,
);
if (original) {
d.i.setState(() => {
d.cv.emoji = structuredClone(original);
d.cv.change = undefined;
});
}
}
}
async handleAddEmojiClick(form: EmojiForm, event: any) {
event.preventDefault();
form.setState(prevState => {
const page =
1 + Math.floor(prevState.customEmojis.length / form.itemsPerPage);
const item: CustomEmojiViewForm = {
id: 0,
shortcode: "",
alt_text: "",
category: "",
image_url: "",
keywords: "",
changed: false,
page: page,
loading: false,
};
return {
...prevState,
customEmojis: [...prevState.customEmojis, item],
page,
};
form.setState(prev => {
prev.emojis.push({
emoji: {
custom_emoji: {
id: -1,
published: "",
category: "",
shortcode: "",
image_url: "",
alt_text: "",
},
keywords: [],
},
change: "create",
});
});
}
@ -489,14 +575,15 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
file = event;
}
form.setState(prevState => ({
...prevState,
customEmojis: prevState.customEmojis.map((cv, i) =>
i === index ? { ...cv, loading: true } : cv,
),
}));
const editable = form.state.emojis[index];
form.setState(() => {
editable.loading = true;
});
HttpService.client.uploadImage({ image: file }).then(res => {
form.setState(() => {
editable.loading = false;
});
if (res.state === "success") {
if (res.data.msg === "ok") {
pictrsDeleteToast(file.name, res.data.delete_url as string);
@ -517,10 +604,20 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
}
configurePicker(): any {
const custom = this.state.emojiMartCustom;
if (process.env["NODE_ENV"] === "development") {
// Once an emoji-mart Picker is initialized with these options, other
// instances also only show the custom emojis.
console.assert(
amAdmin(),
"EmojiMart doesn't deal well with differently configured instances.",
);
}
return {
data: { categories: [], emojis: [], aliases: [] },
maxFrequentRows: 0,
dynamicWidth: true,
custom,
};
}
}

View File

@ -1,5 +1,6 @@
import {
commentsToFlatNodes,
commentToPostSortType,
editComment,
editPost,
editWith,
@ -16,7 +17,6 @@ import {
import {
getQueryParams,
getQueryString,
getRandomFromList,
resourcesSettled,
} from "@utils/helpers";
import { scrollMixin } from "../mixins/scroll-mixin";
@ -67,9 +67,10 @@ import {
RemovePost,
SaveComment,
SavePost,
SortType,
PostSortType,
SuccessResponse,
TransferCommunity,
CommentSortType,
} from "lemmy-js-client";
import { fetchLimit, relTags } from "../../config";
import {
@ -107,6 +108,7 @@ import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes";
import PostHiddenSelect from "../common/post-hidden-select";
import { isBrowser, snapToTop } from "@utils/browser";
import { CommentSortSelect } from "../common/comment-sort-select";
interface HomeState {
postsRes: RequestState<GetPostsResponse>;
@ -122,7 +124,7 @@ interface HomeState {
interface HomeProps {
listingType?: ListingType;
dataType: DataType;
sort: SortType;
sort: PostSortType;
pageCursor?: PaginationCursor;
showHidden?: StringBoolean;
}
@ -132,7 +134,7 @@ type HomeData = RouteDataResponse<{
commentsRes: GetCommentsResponse;
}>;
function getRss(listingType: ListingType, sort: SortType) {
function getRss(listingType: ListingType, sort: PostSortType) {
let rss: string | undefined = undefined;
const queryString = getQueryString({ sort });
@ -177,13 +179,13 @@ function getListingTypeFromQuery(
function getSortTypeFromQuery(
type: string | undefined,
fallback: SortType,
): SortType {
return type ? (type as SortType) : fallback;
fallback: PostSortType,
): PostSortType {
return type ? (type as PostSortType) : fallback;
}
type Fallbacks = {
sort: SortType;
sort: PostSortType;
listingType: ListingType;
};
@ -204,7 +206,8 @@ export function getHomeQueryParams(
},
source,
{
sort: local_user?.default_sort_type ?? local_site.default_sort_type,
sort:
local_user?.default_post_sort_type ?? local_site.default_post_sort_type,
listingType:
local_user?.default_listing_type ??
local_site.default_post_listing_type,
@ -264,6 +267,7 @@ export class Home extends Component<HomeRouteProps, HomeState> {
super(props, context);
this.handleSortChange = this.handleSortChange.bind(this);
this.handleCommentSortChange = this.handleCommentSortChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
this.handleShowHiddenChange = this.handleShowHiddenChange.bind(this);
@ -311,9 +315,7 @@ export class Home extends Component<HomeRouteProps, HomeState> {
};
}
this.state.tagline = getRandomFromList(
this.state?.siteRes?.taglines ?? [],
)?.content;
this.state.tagline = this.state?.siteRes?.tagline?.content;
}
async componentWillMount() {
@ -726,7 +728,14 @@ export class Home extends Component<HomeRouteProps, HomeState> {
/>
</div>
<div className="col-auto">
<SortSelect sort={sort} onChange={this.handleSortChange} />
{this.props.dataType === DataType.Post ? (
<SortSelect sort={sort} onChange={this.handleSortChange} />
) : (
<CommentSortSelect
sort={postToCommentSortType(sort)}
onChange={this.handleCommentSortChange}
/>
)}
</div>
<div className="col-auto ps-0">
{getRss(
@ -799,10 +808,14 @@ export class Home extends Component<HomeRouteProps, HomeState> {
this.updateUrl({ pageCursor: nextPage });
}
handleSortChange(val: SortType) {
handleSortChange(val: PostSortType) {
this.updateUrl({ sort: val, pageCursor: undefined });
}
handleCommentSortChange(val: CommentSortType) {
this.updateUrl({ sort: commentToPostSortType(val), pageCursor: undefined });
}
handleListingTypeChange(val: ListingType) {
this.updateUrl({ listingType: val, pageCursor: undefined });
}

View File

@ -2,7 +2,6 @@ import { capitalizeFirstLetter, validInstanceTLD } from "@utils/helpers";
import {
Component,
InfernoKeyboardEvent,
InfernoMouseEvent,
InfernoNode,
linkEvent,
} from "inferno";
@ -877,42 +876,6 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
this.setState(s => ((s.siteForm.legal_information = val), s));
}
handleTaglineChange(i: SiteForm, index: number, val: string) {
const taglines = i.state.siteForm.taglines;
if (taglines) {
taglines[index] = val;
i.setState(i.state);
}
}
handleDeleteTaglineClick(
i: SiteForm,
index: number,
event: InfernoMouseEvent<HTMLButtonElement>,
) {
event.preventDefault();
const taglines = i.state.siteForm.taglines;
if (taglines) {
taglines.splice(index, 1);
i.state.siteForm.taglines = undefined;
i.setState(i.state);
i.state.siteForm.taglines = taglines;
i.setState(i.state);
}
}
handleAddTaglineClick(
i: SiteForm,
event: InfernoMouseEvent<HTMLButtonElement>,
) {
event.preventDefault();
if (!i.state.siteForm.taglines) {
i.state.siteForm.taglines = [];
}
i.state.siteForm.taglines.push("");
i.setState(i.state);
}
handleSiteApplicationQuestionChange(val: string) {
this.setState(s => ((s.siteForm.application_question = val), s));
}

View File

@ -1,49 +1,84 @@
import { capitalizeFirstLetter } from "@utils/helpers";
import { Component, InfernoMouseEvent, linkEvent } from "inferno";
import { EditSite, Tagline } from "lemmy-js-client";
import { I18NextService } from "../../services";
import { Tagline } from "lemmy-js-client";
import { HttpService, I18NextService } from "../../services";
import { Icon, Spinner } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea";
import { tippyMixin } from "../mixins/tippy-mixin";
import { Paginator } from "../common/paginator";
import classNames from "classnames";
import { isBrowser } from "@utils/browser";
import { Prompt } from "inferno-router";
interface TaglineFormProps {
taglines: Array<Tagline>;
onSaveSite(form: EditSite): void;
loading: boolean;
interface EditableTagline {
change?: "update" | "delete" | "create";
editMode?: boolean;
tagline: Tagline;
}
function markForUpdate(editable: EditableTagline) {
if (editable.change !== "create") {
editable.change = "update";
}
}
interface TaglineFormState {
taglines: Array<string>;
editingRow?: number;
taglines: Array<EditableTagline>;
page: number;
loading: boolean;
}
@tippyMixin
export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
export class TaglineForm extends Component<
Record<never, never>,
TaglineFormState
> {
state: TaglineFormState = {
editingRow: undefined,
taglines: this.props.taglines.map(x => x.content),
taglines: [],
page: 1,
loading: false,
};
constructor(props: any, context: any) {
super(props, context);
this.handlePageChange = this.handlePageChange.bind(this);
}
componentWillMount(): void {
if (isBrowser()) {
this.handlePageChange(1);
}
}
hasPendingChanges(): boolean {
return this.state.taglines.some(x => x.change);
}
render() {
return (
<div className="tagline-form col-12">
<Prompt
message={I18NextService.i18n.t("block_leaving")}
when={this.hasPendingChanges()}
/>
<h1 className="h4 mb-4">{I18NextService.i18n.t("taglines")}</h1>
<div className="table-responsive col-12">
<table id="taglines_table" className="table table-sm table-hover">
<table
id="taglines_table"
className="table table-sm table-hover align-middle"
>
<thead className="pointer">
<th></th>
<th style="width:60px"></th>
<th style="width:121px"></th>
</thead>
<tbody>
{this.state.taglines.map((cv, index) => (
<tr key={index}>
<td>
{this.state.editingRow === index && (
{cv.editMode ? (
<MarkdownTextArea
initialContent={cv}
initialContent={cv.tagline.content}
focus={true}
onContentChange={s =>
this.handleTaglineChange(this, index, s)
}
@ -51,8 +86,32 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
allLanguages={[]}
siteLanguages={[]}
/>
) : (
<div>{cv.tagline.content}</div>
)}
</td>
<td
className={classNames("text-center", {
"border-info": cv.change === "update",
"border-danger": cv.change === "delete",
"border-warning": cv.change === "create",
})}
>
{cv.change === "update" && (
<span>
<Icon icon="transfer" />
</span>
)}
{cv.change === "delete" && (
<span>
<Icon icon="trash" />
</span>
)}
{cv.change === "create" && (
<span>
<Icon icon="add" inline />
</span>
)}
{this.state.editingRow !== index && <div>{cv}</div>}
</td>
<td className="text-right">
<button
@ -99,65 +158,153 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
<button
onClick={linkEvent(this, this.handleSaveClick)}
className="btn btn-secondary me-2"
disabled={this.props.loading}
disabled={this.state.loading || !this.hasPendingChanges()}
>
{this.props.loading ? (
{this.state.loading ? (
<Spinner />
) : (
capitalizeFirstLetter(I18NextService.i18n.t("save"))
)}
</button>
{this.hasPendingChanges() && (
<button
onClick={linkEvent(this, this.handleCancelClick)}
className="btn btn-secondary me-2"
>
{I18NextService.i18n.t("cancel")}
</button>
)}
</div>
</div>
<div>
<Paginator
page={this.state.page}
onChange={this.handlePageChange}
nextDisabled={false}
disabled={this.hasPendingChanges()}
/>
</div>
</div>
</div>
);
}
handleTaglineChange(i: TaglineForm, index: number, val: string) {
if (i.state.taglines) {
i.setState(prev => ({
...prev,
taglines: prev.taglines.map((tl, i) => (i === index ? val : tl)),
}));
}
const editable = i.state.taglines[index];
i.setState(() => {
markForUpdate(editable);
const tagline: Tagline = editable.tagline;
tagline.content = val;
});
}
handleDeleteTaglineClick(d: { i: TaglineForm; index: number }, event: any) {
async handleDeleteTaglineClick(
d: { i: TaglineForm; index: number },
event: any,
) {
event.preventDefault();
d.i.setState(prev => ({
...prev,
taglines: prev.taglines.filter((_, i) => i !== d.index),
editingRow: undefined,
}));
const editable = d.i.state.taglines[d.index];
if (editable.change === "create") {
// This drops the entry immediately, other deletes have to be saved.
d.i.setState(prev => {
return { taglines: prev.taglines.filter(x => x !== editable) };
});
} else {
d.i.setState(() => {
editable.change = "delete";
editable.editMode = false;
});
}
}
handleEditTaglineClick(d: { i: TaglineForm; index: number }, event: any) {
event.preventDefault();
if (d.i.state.editingRow === d.index) {
d.i.setState({ editingRow: undefined });
} else {
d.i.setState({ editingRow: d.index });
}
}
async handleSaveClick(i: TaglineForm) {
i.props.onSaveSite({
taglines: i.state.taglines,
const editable = d.i.state.taglines[d.index];
d.i.setState(prev => {
prev.taglines
.filter(x => x !== editable)
.forEach(x => {
x.editMode = false;
});
editable.editMode = !editable.editMode;
});
}
handleAddTaglineClick(
async handleSaveClick(i: TaglineForm) {
const promises: Promise<any>[] = [];
for (const editable of i.state.taglines) {
if (editable.change === "update") {
promises.push(
HttpService.client.editTagline(editable.tagline).then(res => {
if (res.state === "success") {
i.setState(() => {
editable.change = undefined;
editable.tagline = res.data.tagline;
});
}
}),
);
} else if (editable.change === "delete") {
promises.push(
HttpService.client.deleteTagline(editable.tagline).then(res => {
if (res.state === "success") {
i.setState(() => {
editable.change = undefined;
return {
taglines: this.state.taglines.filter(x => x !== editable),
};
});
}
}),
);
} else if (editable.change === "create") {
promises.push(
HttpService.client.createTagline(editable.tagline).then(res => {
if (res.state === "success") {
i.setState(() => {
editable.change = undefined;
editable.tagline = res.data.tagline;
});
}
}),
);
}
}
await Promise.all(promises);
}
async handleCancelClick(i: TaglineForm) {
i.handlePageChange(i.state.page);
}
async handleAddTaglineClick(
i: TaglineForm,
event: InfernoMouseEvent<HTMLButtonElement>,
) {
event.preventDefault();
const newTaglines = [...i.state.taglines];
newTaglines.push("");
i.setState({
taglines: newTaglines,
editingRow: newTaglines.length - 1,
i.setState(prev => {
prev.taglines.forEach(x => {
x.editMode = false;
});
prev.taglines.push({
tagline: { id: -1, content: "", published: "" },
change: "create",
editMode: true,
});
});
}
async handlePageChange(val: number) {
this.setState({ loading: true });
const taglineRes = await HttpService.client.listTaglines({ page: val });
if (taglineRes.state === "success") {
this.setState({
page: val,
loading: false,
taglines: taglineRes.data.taglines.map(t => ({ tagline: t })),
});
} else {
this.setState({ loading: false });
}
}
}

View File

@ -37,7 +37,7 @@ import {
RemovePost,
SaveComment,
SavePost,
SortType,
PostSortType,
TransferCommunity,
} from "lemmy-js-client";
import { CommentViewType, PersonDetailsView } from "../../interfaces";
@ -53,7 +53,7 @@ interface PersonDetailsProps {
siteLanguages: number[];
page: number;
limit: number;
sort: SortType;
sort: PostSortType;
enableDownvotes: boolean;
voteDisplayMode: LocalUserVoteDisplayMode;
enableNsfw: boolean;

View File

@ -70,7 +70,7 @@ import {
RemovePost,
SaveComment,
SavePost,
SortType,
PostSortType,
SuccessResponse,
TransferCommunity,
RegistrationApplicationResponse,
@ -127,7 +127,7 @@ interface ProfileState {
interface ProfileProps {
view: PersonDetailsView;
sort: SortType;
sort: PostSortType;
page: number;
}
@ -142,8 +142,8 @@ export function getProfileQueryParams(source?: string): ProfileProps {
);
}
function getSortTypeFromQuery(sort?: string): SortType {
return sort ? (sort as SortType) : "New";
function getSortTypeFromQuery(sort?: string): PostSortType {
return sort ? (sort as PostSortType) : "New";
}
function getViewFromProps(view?: string): PersonDetailsView {
@ -956,7 +956,7 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
this.updateUrl({ page });
}
handleSortChange(sort: SortType) {
handleSortChange(sort: PostSortType) {
this.updateUrl({ sort, page: 1 });
}

View File

@ -22,6 +22,7 @@ import {
BlockCommunityResponse,
BlockInstanceResponse,
BlockPersonResponse,
CommentSortType,
Community,
GenerateTotpSecretResponse,
GetFederatedInstancesResponse,
@ -31,7 +32,7 @@ import {
ListingType,
LoginResponse,
Person,
SortType,
PostSortType,
SuccessResponse,
UpdateTotpResponse,
} from "lemmy-js-client";
@ -76,6 +77,7 @@ import { getHttpBaseInternal } from "../../utils/env";
import { IRoutePropsWithFetch } from "../../routes";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { simpleScrollMixin } from "../mixins/scroll-mixin";
import { CommentSortSelect } from "../common/comment-sort-select";
type SettingsData = RouteDataResponse<{
instancesRes: GetFederatedInstancesResponse;
@ -94,7 +96,8 @@ interface SettingsState {
blur_nsfw?: boolean;
auto_expand?: boolean;
theme?: string;
default_sort_type?: SortType;
default_post_sort_type?: PostSortType;
default_comment_sort_type?: CommentSortType;
default_listing_type?: ListingType;
interface_language?: string;
avatar?: string;
@ -248,7 +251,9 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
constructor(props: any, context: any) {
super(props, context);
this.handleSortTypeChange = this.handleSortTypeChange.bind(this);
this.handlePostSortTypeChange = this.handlePostSortTypeChange.bind(this);
this.handleCommentSortTypeChange =
this.handleCommentSortTypeChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
this.handleBioChange = this.handleBioChange.bind(this);
this.handleDiscussionLanguageChange =
@ -276,9 +281,9 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
local_user: {
show_nsfw,
blur_nsfw,
auto_expand,
theme,
default_sort_type,
default_post_sort_type,
default_comment_sort_type,
default_listing_type,
interface_language,
show_avatars,
@ -313,9 +318,9 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
...this.state.saveUserSettingsForm,
show_nsfw,
blur_nsfw,
auto_expand,
theme: theme ?? "browser",
default_sort_type,
default_post_sort_type,
default_comment_sort_type,
default_listing_type,
interface_language,
discussion_languages: mui.discussion_languages,
@ -905,14 +910,29 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
</form>
<form className="mb-3 row">
<label className="col-sm-3 col-form-label">
{I18NextService.i18n.t("sort_type")}
{I18NextService.i18n.t("post_sort_type")}
</label>
<div className="col-sm-9">
<SortSelect
sort={
this.state.saveUserSettingsForm.default_sort_type ?? "Active"
this.state.saveUserSettingsForm.default_post_sort_type ??
"Active"
}
onChange={this.handleSortTypeChange}
onChange={this.handlePostSortTypeChange}
/>
</div>
</form>
<form className="mb-3 row">
<label className="col-sm-3 col-form-label">
{I18NextService.i18n.t("comment_sort_type")}
</label>
<div className="col-sm-9">
<CommentSortSelect
sort={
this.state.saveUserSettingsForm.default_comment_sort_type ??
"Hot"
}
onChange={this.handleCommentSortTypeChange}
/>
</div>
</form>
@ -1580,8 +1600,16 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
);
}
handleSortTypeChange(val: SortType) {
this.setState(s => ((s.saveUserSettingsForm.default_sort_type = val), s));
handlePostSortTypeChange(val: PostSortType) {
this.setState(
s => ((s.saveUserSettingsForm.default_post_sort_type = val), s),
);
}
handleCommentSortTypeChange(val: CommentSortType) {
this.setState(
s => ((s.saveUserSettingsForm.default_comment_sort_type = val), s),
);
}
handleListingTypeChange(val: ListingType) {
@ -1743,9 +1771,9 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
local_user: {
show_nsfw,
blur_nsfw,
auto_expand,
theme,
default_sort_type,
default_post_sort_type,
default_comment_sort_type,
default_listing_type,
interface_language,
show_avatars,
@ -1782,11 +1810,11 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
display_name,
bio,
matrix_user_id,
auto_expand,
blur_nsfw,
bot_account,
default_listing_type,
default_sort_type,
default_post_sort_type,
default_comment_sort_type,
discussion_languages: siteRes.data.my_user?.discussion_languages,
email,
interface_language,

View File

@ -148,10 +148,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
UserService.Instance.myUserInfo &&
!this.isoData.showAdultConsentModal
) {
const { auto_expand, blur_nsfw } =
UserService.Instance.myUserInfo.local_user_view.local_user;
const blur_nsfw =
UserService.Instance.myUserInfo.local_user_view.local_user.blur_nsfw;
this.setState({
imageExpanded: auto_expand && !(blur_nsfw && this.postView.post.nsfw),
imageExpanded: !(blur_nsfw && this.postView.post.nsfw),
});
}

View File

@ -124,11 +124,12 @@ interface PostState {
lastCreatedCommentId?: CommentId;
}
const defaultCommentSort: CommentSortType = "Hot";
function getCommentSortTypeFromQuery(source?: string): CommentSortType {
function getCommentSortTypeFromQuery(
source: string | undefined,
fallback: CommentSortType,
): CommentSortType {
if (!source) {
return defaultCommentSort;
return fallback;
}
switch (source) {
case "Hot":
@ -138,14 +139,21 @@ function getCommentSortTypeFromQuery(source?: string): CommentSortType {
case "Controversial":
return source;
default:
return defaultCommentSort;
return fallback;
}
}
function getQueryStringFromCommentSortType(
sort: CommentSortType,
siteRes: GetSiteResponse,
): undefined | string {
if (sort === defaultCommentSort) {
const myUserInfo = siteRes.my_user ?? UserService.Instance.myUserInfo;
const local_user = myUserInfo?.local_user_view.local_user;
const local_site = siteRes.site_view.local_site;
const defaultSort =
local_user?.default_comment_sort_type ??
local_site.default_comment_sort_type;
if (sort === defaultSort) {
return undefined;
}
return sort;
@ -185,14 +193,31 @@ interface PostProps {
view: CommentViewType;
scrollToComments: boolean;
}
export function getPostQueryParams(source: string | undefined): PostProps {
return getQueryParams<PostProps>(
type Fallbacks = {
sort: CommentSortType;
};
export function getPostQueryParams(
source: string | undefined,
siteRes: GetSiteResponse,
): PostProps {
const myUserInfo = siteRes.my_user ?? UserService.Instance.myUserInfo;
const local_user = myUserInfo?.local_user_view.local_user;
const local_site = siteRes.site_view.local_site;
return getQueryParams<PostProps, Fallbacks>(
{
scrollToComments: (s?: string) => !!s,
sort: getCommentSortTypeFromQuery,
view: getCommentViewTypeFromQuery,
},
source,
{
sort:
local_user?.default_comment_sort_type ??
local_site.default_comment_sort_type,
},
);
}
@ -325,7 +350,7 @@ export class Post extends Component<PostRouteProps, PostState> {
};
const query: QueryParams<PostProps> = {
sort: getQueryStringFromCommentSortType(sort),
sort: getQueryStringFromCommentSortType(sort, this.state.siteRes),
view: getQueryStringFromCommentView(view),
};

View File

@ -46,7 +46,7 @@ import {
Search as SearchForm,
SearchResponse,
SearchType,
SortType,
PostSortType,
} from "lemmy-js-client";
import { fetchLimit } from "../config";
import { CommentViewType, InitialFetchRequest } from "../interfaces";
@ -76,9 +76,9 @@ import { isBrowser } from "@utils/browser";
interface SearchProps {
q?: string;
type: SearchType;
sort: SortType;
sort: PostSortType;
listingType: ListingType;
postTitleOnly?: boolean;
titleOnly?: boolean;
communityId?: number;
creatorId?: number;
page: number;
@ -124,7 +124,7 @@ export function getSearchQueryParams(source?: string): SearchProps {
type: getSearchTypeFromQuery,
sort: getSortTypeFromQuery,
listingType: getListingTypeFromQuery,
postTitleOnly: getBoolFromString,
titleOnly: getBoolFromString,
communityId: getIdFromString,
creatorId: getIdFromString,
page: getPageFromString,
@ -139,8 +139,8 @@ function getSearchTypeFromQuery(type_?: string): SearchType {
return type_ ? (type_ as SearchType) : defaultSearchType;
}
function getSortTypeFromQuery(sort?: string): SortType {
return sort ? (sort as SortType) : defaultSortType;
function getSortTypeFromQuery(sort?: string): PostSortType {
return sort ? (sort as PostSortType) : defaultSortType;
}
function getListingTypeFromQuery(listingType?: string): ListingType {
@ -286,7 +286,7 @@ export class Search extends Component<SearchRouteProps, SearchState> {
this.handleCommunityFilterChange =
this.handleCommunityFilterChange.bind(this);
this.handleCreatorFilterChange = this.handleCreatorFilterChange.bind(this);
this.handlePostTitleChange = this.handlePostTitleChange.bind(this);
this.handleTitleOnlyChange = this.handleTitleOnlyChange.bind(this);
// Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) {
@ -473,7 +473,7 @@ export class Search extends Component<SearchRouteProps, SearchState> {
type: searchType,
sort,
listingType: listing_type,
postTitleOnly: post_title_only,
titleOnly: title_only,
communityId: community_id,
creatorId: creator_id,
page,
@ -519,7 +519,7 @@ export class Search extends Component<SearchRouteProps, SearchState> {
type_: searchType,
sort,
listing_type,
post_title_only,
title_only,
page,
limit: fetchLimit,
};
@ -595,7 +595,6 @@ export class Search extends Component<SearchRouteProps, SearchState> {
case "Comments":
return this.comments;
case "Posts":
case "Url":
return this.posts;
case "Communities":
return this.communities;
@ -641,7 +640,7 @@ export class Search extends Component<SearchRouteProps, SearchState> {
}
get selects() {
const { type, listingType, postTitleOnly, sort, communityId, creatorId } =
const { type, listingType, titleOnly, sort, communityId, creatorId } =
this.props;
const {
communitySearchOptions,
@ -684,15 +683,12 @@ export class Search extends Component<SearchRouteProps, SearchState> {
<div className="col">
<input
className="btn-check"
id="post-title-only"
id="title-only"
type="checkbox"
checked={postTitleOnly}
onChange={this.handlePostTitleChange}
checked={titleOnly}
onChange={this.handleTitleOnlyChange}
/>
<label
className="btn btn-outline-secondary"
htmlFor="post-title-only"
>
<label className="btn btn-outline-secondary" htmlFor="title-only">
{I18NextService.i18n.t("post_title_only")}
</label>
</div>
@ -1074,7 +1070,7 @@ export class Search extends Component<SearchRouteProps, SearchState> {
type,
sort,
listingType,
postTitleOnly,
titleOnly,
page,
} = props;
@ -1087,7 +1083,7 @@ export class Search extends Component<SearchRouteProps, SearchState> {
type_: type,
sort,
listing_type: listingType,
post_title_only: postTitleOnly,
title_only: titleOnly,
page,
limit: fetchLimit,
});
@ -1152,13 +1148,13 @@ export class Search extends Component<SearchRouteProps, SearchState> {
return this.searchInput.current?.value ?? this.props.q;
}
handleSortChange(sort: SortType) {
handleSortChange(sort: PostSortType) {
this.updateUrl({ sort, page: 1, q: this.getQ() });
}
handlePostTitleChange(event: any) {
const postTitleOnly = event.target.checked;
this.updateUrl({ postTitleOnly, q: this.getQ() });
handleTitleOnlyChange(event: any) {
const titleOnly = event.target.checked;
this.updateUrl({ titleOnly, q: this.getQ() });
}
handleTypeChange(i: Search, event: any) {
@ -1213,7 +1209,7 @@ export class Search extends Component<SearchRouteProps, SearchState> {
q,
type,
listingType,
postTitleOnly,
titleOnly,
sort,
communityId,
creatorId,
@ -1227,7 +1223,7 @@ export class Search extends Component<SearchRouteProps, SearchState> {
q,
type: type,
listingType: listingType,
postTitleOnly: postTitleOnly?.toString(),
titleOnly: titleOnly?.toString(),
communityId: communityId?.toString(),
creatorId: creatorId?.toString(),
page: page?.toString(),

View File

@ -17,6 +17,8 @@ import markdown_it_highlightjs from "markdown-it-highlightjs/core";
import { Renderer, Token } from "markdown-it";
import { instanceLinkRegex, relTags } from "./config";
import { lazyHighlightjs } from "./lazy-highlightjs";
import { HttpService } from "./services";
import { WrappedLemmyHttp } from "./services/HttpService";
let Tribute: any;
@ -32,7 +34,7 @@ export const mdLimited: MarkdownIt = new MarkdownIt("zero").enable([
"strikethrough",
]);
export const customEmojis: EmojiMartCategory[] = [];
let customEmojis: EmojiMartCategory[] = [];
export let customEmojisLookup: Map<string, CustomEmojiView> = new Map<
string,
@ -204,14 +206,17 @@ export function setupMarkdown() {
) {
//Provide custom renderer for our emojis to allow us to add a css class and force size dimensions on them.
const item = tokens[idx] as any;
let title = item.attrs.length >= 3 ? item.attrs[2][1] : "";
const url = item.attrs.length > 0 ? item.attrs[0][1] : "";
const altText = item.attrs.length > 1 ? item.attrs[1][1] : "";
const title = item.attrs.length > 2 ? item.attrs[2][1] : "";
const splitTitle = title.split(/ (.*)/, 2);
const isEmoji = splitTitle[0] === "emoji";
let shortcode: string | undefined;
if (isEmoji) {
title = splitTitle[1];
shortcode = splitTitle[1];
}
const customEmoji = customEmojisLookup.get(title);
const isLocalEmoji = customEmoji !== undefined;
// customEmojisLookup is empty in SSR, CSR rerenders markdown anyway
const isLocalEmoji = shortcode && customEmojisLookup.has(shortcode);
if (!isLocalEmoji) {
const imgElement =
defaultImageRenderer?.(tokens, idx, options, env, self) ?? "";
@ -222,10 +227,8 @@ export function setupMarkdown() {
} else return "";
}
return `<img class="icon icon-emoji" src="${
customEmoji!.custom_emoji.image_url
}" title="${customEmoji!.custom_emoji.shortcode}" alt="${
customEmoji!.custom_emoji.alt_text
}"/>`;
url
}" title="${shortcode}" alt="${altText}"/>`;
};
md.renderer.rules.table_open = function () {
return '<table class="table">';
@ -247,12 +250,14 @@ export function setupMarkdown() {
};
}
export function setupEmojiDataModel(custom_emoji_views: CustomEmojiView[]) {
export function emojiMartCategories(
custom_emoji_views: CustomEmojiView[],
): EmojiMartCategory[] {
const groupedEmojis = groupBy(
custom_emoji_views,
x => x.custom_emoji.category,
);
customEmojis.length = 0;
const customEmojis: EmojiMartCategory[] = [];
for (const [category, emojis] of Object.entries(groupedEmojis)) {
customEmojis.push({
id: category,
@ -265,63 +270,24 @@ export function setupEmojiDataModel(custom_emoji_views: CustomEmojiView[]) {
})),
});
}
return customEmojis;
}
export async function setupEmojiDataModel(
client: WrappedLemmyHttp = HttpService.client,
): Promise<boolean> {
const emojisRes = await client.listCustomEmojis({
ignore_page_limits: true,
});
if (emojisRes.state !== "success") {
return false;
}
const custom_emoji_views = emojisRes.data.custom_emojis;
customEmojis = emojiMartCategories(custom_emoji_views);
customEmojisLookup = new Map(
custom_emoji_views.map(view => [view.custom_emoji.shortcode, view]),
);
}
export function updateEmojiDataModel(custom_emoji_view: CustomEmojiView) {
const emoji: EmojiMartCustomEmoji = {
id: custom_emoji_view.custom_emoji.shortcode,
name: custom_emoji_view.custom_emoji.shortcode,
keywords: custom_emoji_view.keywords.map(x => x.keyword),
skins: [{ src: custom_emoji_view.custom_emoji.image_url }],
};
const categoryIndex = customEmojis.findIndex(
x => x.id === custom_emoji_view.custom_emoji.category,
);
if (categoryIndex === -1) {
customEmojis.push({
id: custom_emoji_view.custom_emoji.category,
name: custom_emoji_view.custom_emoji.category,
emojis: [emoji],
});
} else {
const emojiIndex = customEmojis[categoryIndex].emojis.findIndex(
x => x.id === custom_emoji_view.custom_emoji.shortcode,
);
if (emojiIndex === -1) {
customEmojis[categoryIndex].emojis.push(emoji);
} else {
customEmojis[categoryIndex].emojis[emojiIndex] = emoji;
}
}
customEmojisLookup.set(
custom_emoji_view.custom_emoji.shortcode,
custom_emoji_view,
);
}
export function removeFromEmojiDataModel(id: number) {
let view: CustomEmojiView | undefined;
for (const item of customEmojisLookup.values()) {
if (item.custom_emoji.id === id) {
view = item;
break;
}
}
if (!view) return;
const categoryIndex = customEmojis.findIndex(
x => x.id === view?.custom_emoji.category,
);
const emojiIndex = customEmojis[categoryIndex].emojis.findIndex(
x => x.id === view?.custom_emoji.shortcode,
);
customEmojis[categoryIndex].emojis = customEmojis[
categoryIndex
].emojis.splice(emojiIndex, 1);
customEmojisLookup.delete(view?.custom_emoji.shortcode);
return true;
}
export function getEmojiMart(
@ -329,9 +295,9 @@ export function getEmojiMart(
customPickerOptions: any = {},
) {
const pickerOptions = {
...customPickerOptions,
onEmojiSelect: onEmojiSelect,
custom: customEmojis,
...customPickerOptions,
};
return new Picker(pickerOptions);
}
@ -416,7 +382,7 @@ export async function setupTribute() {
});
}
interface EmojiMartCategory {
export interface EmojiMartCategory {
id: string;
name: string;
emojis: EmojiMartCustomEmoji[];

View File

@ -0,0 +1,21 @@
import { CommentSortType, PostSortType } from "lemmy-js-client";
function assertType<T>(_: T) {}
export default function commentToPostSortType(
sort: CommentSortType,
): PostSortType {
switch (sort) {
case "Hot":
case "New":
case "Old":
case "Controversial":
return sort;
case "Top":
return "TopAll";
default: {
assertType<never>(sort);
return "Hot";
}
}
}

View File

@ -1,31 +0,0 @@
import { CommentSortType, SortType } from "lemmy-js-client";
export default function convertCommentSortType(
sort: SortType,
): CommentSortType {
switch (sort) {
case "TopAll":
case "TopHour":
case "TopSixHour":
case "TopTwelveHour":
case "TopDay":
case "TopWeek":
case "TopMonth":
case "TopThreeMonths":
case "TopSixMonths":
case "TopNineMonths":
case "TopYear": {
return "Top";
}
case "New": {
return "New";
}
case "Hot":
case "Active": {
return "Hot";
}
default: {
return "Hot";
}
}
}

View File

@ -1,11 +1,11 @@
import buildCommentsTree from "./build-comments-tree";
import { colorList } from "./color-list";
import commentToPostSortType from "./comment-to-post-sort-type";
import commentsToFlatNodes from "./comments-to-flat-nodes";
import communityRSSUrl from "./community-rss-url";
import communitySearch from "./community-search";
import communitySelectName from "./community-select-name";
import communityToChoice from "./community-to-choice";
import convertCommentSortType from "./convert-comment-sort-type";
import editComment from "./edit-comment";
import editCommentReply from "./edit-comment-reply";
import editCommentReport from "./edit-comment-report";
@ -59,12 +59,12 @@ import isAnonymousPath from "./is-anonymous-path";
export {
buildCommentsTree,
colorList,
commentToPostSortType,
commentsToFlatNodes,
communityRSSUrl,
communitySearch,
communitySelectName,
communityToChoice,
convertCommentSortType,
editComment,
editCommentReply,
editCommentReport,

View File

@ -1,11 +1,8 @@
import { GetSiteResponse } from "lemmy-js-client";
import { setupEmojiDataModel, setupMarkdown } from "../../markdown";
import { setupMarkdown } from "../../markdown";
import { UserService } from "../../services";
export default function initializeSite(site?: GetSiteResponse) {
UserService.Instance.myUserInfo = site?.my_user;
if (site) {
setupEmojiDataModel(site.custom_emojis ?? []);
}
setupMarkdown();
}

View File

@ -1,16 +1,39 @@
import { CommentSortType, SortType } from "lemmy-js-client";
import { CommentSortType, PostSortType } from "lemmy-js-client";
export default function postToCommentSortType(sort: SortType): CommentSortType {
function assertType<T>(_: T) {}
export default function postToCommentSortType(
sort: PostSortType,
): CommentSortType {
switch (sort) {
case "Active":
case "Hot":
return "Hot";
case "New":
case "NewComments":
return "New";
case "Old":
return "Old";
default:
case "Controversial": {
return sort;
}
case "TopAll":
case "TopHour":
case "TopSixHour":
case "TopTwelveHour":
case "TopDay":
case "TopWeek":
case "TopMonth":
case "TopThreeMonths":
case "TopSixMonths":
case "TopNineMonths":
case "TopYear": {
return "Top";
}
case "NewComments":
case "MostComments":
case "Scaled":
case "Active": {
return "Hot";
}
default: {
assertType<never>(sort);
return "Hot";
}
}
}