Merge branch 'main' into add_ban_from_community_string

This commit is contained in:
Dessalines 2023-02-21 15:54:16 -05:00
commit 42bb7a3974
8 changed files with 167 additions and 197 deletions

@ -1 +1 @@
Subproject commit 0446b27d493b4f4fb6f227f538e6604860c7fe41 Subproject commit 7379716231b9f7e67f710751c839398b7ab5d65e

View file

@ -305,16 +305,6 @@ pre {
transition: width 0.2s ease-out 0s !important; transition: width 0.2s ease-out 0s !important;
} }
.show-input {
width: 13vw !important;
}
.hide-input {
background: transparent !important;
background-color: transparent !important;
width: 0px !important;
padding: 0 !important;
}
br.big { br.big {
display: block; display: block;
content: ""; content: "";

View file

@ -1,4 +1,4 @@
import { Component, createRef, linkEvent, RefObject } from "inferno"; import { Component, linkEvent } from "inferno";
import { NavLink } from "inferno-router"; import { NavLink } from "inferno-router";
import { import {
CommentResponse, CommentResponse,
@ -44,7 +44,6 @@ interface NavbarState {
unreadReportCount: number; unreadReportCount: number;
unreadApplicationCount: number; unreadApplicationCount: number;
searchParam: string; searchParam: string;
toggleSearch: boolean;
showDropdown: boolean; showDropdown: boolean;
onSiteBanner?(url: string): any; onSiteBanner?(url: string): any;
} }
@ -55,14 +54,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
private unreadInboxCountSub: Subscription; private unreadInboxCountSub: Subscription;
private unreadReportCountSub: Subscription; private unreadReportCountSub: Subscription;
private unreadApplicationCountSub: Subscription; private unreadApplicationCountSub: Subscription;
private searchTextField: RefObject<HTMLInputElement>;
state: NavbarState = { state: NavbarState = {
unreadInboxCount: 0, unreadInboxCount: 0,
unreadReportCount: 0, unreadReportCount: 0,
unreadApplicationCount: 0, unreadApplicationCount: 0,
expanded: false, expanded: false,
searchParam: "", searchParam: "",
toggleSearch: false,
showDropdown: false, showDropdown: false,
}; };
subscription: any; subscription: any;
@ -77,8 +74,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
componentDidMount() { componentDidMount() {
// Subscribe to jwt changes // Subscribe to jwt changes
if (isBrowser()) { if (isBrowser()) {
this.searchTextField = createRef();
// On the first load, check the unreads // On the first load, check the unreads
let auth = myAuth(false); let auth = myAuth(false);
if (auth && UserService.Instance.myUserInfo) { if (auth && UserService.Instance.myUserInfo) {
@ -123,7 +118,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
updateUrl() { updateUrl() {
const searchParam = this.state.searchParam; const searchParam = this.state.searchParam;
this.setState({ searchParam: "" }); this.setState({ searchParam: "" });
this.setState({ toggleSearch: false });
this.setState({ showDropdown: false, expanded: false }); this.setState({ showDropdown: false, expanded: false });
if (searchParam === "") { if (searchParam === "") {
this.context.router.history.push(`/search/`); this.context.router.history.push(`/search/`);
@ -308,35 +302,13 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
) && ( ) && (
<ul className="navbar-nav"> <ul className="navbar-nav">
<li className="nav-item"> <li className="nav-item">
<form <NavLink
className="form-inline mr-1" to="/search"
onSubmit={linkEvent(this, this.handleSearchSubmit)} className="nav-link"
title={i18n.t("search")}
> >
<input <Icon icon="search" />
id="search-input" </NavLink>
className={`form-control mr-0 search-input ${
this.state.toggleSearch ? "show-input" : "hide-input"
}`}
onInput={linkEvent(this, this.handleSearchParam)}
value={this.state.searchParam}
ref={this.searchTextField}
disabled={!this.state.toggleSearch}
type="text"
placeholder={i18n.t("search")}
onBlur={linkEvent(this, this.handleSearchBlur)}
></input>
<label className="sr-only" htmlFor="search-input">
{i18n.t("search")}
</label>
<button
name="search-btn"
onClick={linkEvent(this, this.handleSearchBtn)}
className="px-1 btn btn-link nav-link"
aria-label={i18n.t("search")}
>
<Icon icon="search" />
</button>
</form>
</li> </li>
</ul> </ul>
)} )}
@ -520,28 +492,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
i.setState({ searchParam: event.target.value }); i.setState({ searchParam: event.target.value });
} }
handleSearchSubmit(i: Navbar, event: any) {
event.preventDefault();
i.updateUrl();
}
handleSearchBtn(i: Navbar, event: any) {
event.preventDefault();
i.setState({ toggleSearch: true });
i.searchTextField.current?.focus();
const offsetWidth = i.searchTextField.current?.offsetWidth;
if (i.state.searchParam && offsetWidth && offsetWidth > 100) {
i.updateUrl();
}
}
handleSearchBlur(i: Navbar, event: any) {
if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
i.setState({ toggleSearch: false });
}
}
handleLogoutClick(i: Navbar) { handleLogoutClick(i: Navbar) {
i.setState({ showDropdown: false, expanded: false }); i.setState({ showDropdown: false, expanded: false });
UserService.Instance.logout(); UserService.Instance.logout();

View file

@ -163,27 +163,29 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
? i18n.t("purge_comment") ? i18n.t("purge_comment")
: `${i18n.t("purge")} ${cv.creator.name}`; : `${i18n.t("purge")} ${cv.creator.name}`;
let canMod_ = canMod( let canMod_ =
cv.creator.id, canMod(cv.creator.id, this.props.moderators, this.props.admins) &&
this.props.moderators, cv.community.local;
this.props.admins let canModOnSelf =
); canMod(
let canModOnSelf = canMod( cv.creator.id,
cv.creator.id, this.props.moderators,
this.props.moderators, this.props.admins,
this.props.admins, UserService.Instance.myUserInfo,
UserService.Instance.myUserInfo, true
true ) && cv.community.local;
); let canAdmin_ =
let canAdmin_ = canAdmin(cv.creator.id, this.props.admins); canAdmin(cv.creator.id, this.props.admins) && cv.community.local;
let canAdminOnSelf = canAdmin( let canAdminOnSelf =
cv.creator.id, canAdmin(
this.props.admins, cv.creator.id,
UserService.Instance.myUserInfo, this.props.admins,
true UserService.Instance.myUserInfo,
); true
) && cv.community.local;
let isMod_ = isMod(cv.creator.id, this.props.moderators); let isMod_ = isMod(cv.creator.id, this.props.moderators);
let isAdmin_ = isAdmin(cv.creator.id, this.props.admins); let isAdmin_ =
isAdmin(cv.creator.id, this.props.admins) && cv.community.local;
let amCommunityCreator_ = amCommunityCreator( let amCommunityCreator_ = amCommunityCreator(
cv.creator.id, cv.creator.id,
this.props.moderators this.props.moderators

View file

@ -47,33 +47,38 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
return this.props.iconVersion ? ( return this.props.iconVersion ? (
this.selectBtn this.selectBtn
) : ( ) : (
<div className="form-group row"> <div>
<label <div className="alert alert-warning" role="alert">
className={classNames("col-form-label", { {i18n.t("undetermined_language_warning")}
"col-sm-3": this.props.multiple, </div>
"col-sm-2": !this.props.multiple, <div className="form-group row">
})} <label
htmlFor={this.id} className={classNames("col-form-label", {
> "col-sm-3": this.props.multiple,
{i18n.t(this.props.multiple ? "language_plural" : "language")} "col-sm-2": !this.props.multiple,
</label> })}
<div htmlFor={this.id}
className={classNames("input-group", { >
"col-sm-9": this.props.multiple, {i18n.t(this.props.multiple ? "language_plural" : "language")}
"col-sm-10": !this.props.multiple, </label>
})} <div
> className={classNames("input-group", {
{this.selectBtn} "col-sm-9": this.props.multiple,
{this.props.multiple && ( "col-sm-10": !this.props.multiple,
<div className="input-group-append"> })}
<button >
className="input-group-text" {this.selectBtn}
onClick={linkEvent(this, this.handleDeselectAll)} {this.props.multiple && (
> <div className="input-group-append">
<Icon icon="x" /> <button
</button> className="input-group-text"
</div> onClick={linkEvent(this, this.handleDeselectAll)}
)} >
<Icon icon="x" />
</button>
</div>
)}
</div>
</div> </div>
</div> </div>
); );

View file

@ -30,6 +30,7 @@ import { UserService, WebSocketService } from "../../services";
import { import {
amAdmin, amAdmin,
amCommunityCreator, amCommunityCreator,
amMod,
canAdmin, canAdmin,
canMod, canMod,
futureDaysToUnixTime, futureDaysToUnixTime,
@ -434,15 +435,18 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
let post = this.props.post_view.post; let post = this.props.post_view.post;
return ( return (
<Link <Link
className={ className={`d-inline-block ${
!post.featured_community && !post.featured_local !post.featured_community && !post.featured_local
? "text-body" ? "text-body"
: "text-primary" : "text-primary"
} }`}
to={`/post/${post.id}`} to={`/post/${post.id}`}
title={i18n.t("comments")} title={i18n.t("comments")}
> >
<div dangerouslySetInnerHTML={mdToHtmlInline(post.name)} /> <div
className="d-inline-block"
dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
/>
</Link> </Link>
); );
} }
@ -457,16 +461,19 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{url ? ( {url ? (
this.props.showBody ? ( this.props.showBody ? (
<a <a
className={ className={`d-inline-block ${
!post.featured_community && !post.featured_local !post.featured_community && !post.featured_local
? "text-body" ? "text-body"
: "text-primary" : "text-primary"
} }`}
href={url} href={url}
title={url} title={url}
rel={relTags} rel={relTags}
> >
<div dangerouslySetInnerHTML={mdToHtmlInline(post.name)} /> <div
className="d-inline-block"
dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
/>
</a> </a>
) : ( ) : (
this.postLink this.postLink
@ -477,7 +484,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{(url && isImage(url)) || {(url && isImage(url)) ||
(post.thumbnail_url && ( (post.thumbnail_url && (
<button <button
className="btn btn-link text-monospace text-muted small d-inline-block ml-2" className="btn btn-link text-monospace text-muted small d-inline-block"
data-tippy-content={i18n.t("expand_here")} data-tippy-content={i18n.t("expand_here")}
onClick={linkEvent(this, this.handleImageExpandClick)} onClick={linkEvent(this, this.handleImageExpandClick)}
> >
@ -608,7 +615,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{this.state.showAdvanced && ( {this.state.showAdvanced && (
<> <>
{this.showBody && post_view.post.body && this.viewSourceButton} {this.showBody && post_view.post.body && this.viewSourceButton}
{this.canModOnSelf_ && ( {/* Any mod can do these, not limited to hierarchy*/}
{(amMod(this.props.moderators) || amAdmin()) && (
<> <>
{this.lockButton} {this.lockButton}
{this.featureButton} {this.featureButton}
@ -842,41 +850,40 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
get featureButton() { get featureButton() {
const featured_community = this.props.post_view.post.featured_community; const featuredCommunity = this.props.post_view.post.featured_community;
const label_community = featured_community const labelCommunity = featuredCommunity
? i18n.t("unfeature_from_community") ? i18n.t("unfeature_from_community")
: i18n.t("feature_in_community"); : i18n.t("feature_in_community");
const is_admin = amAdmin(); const featuredLocal = this.props.post_view.post.featured_local;
const featured_local = this.props.post_view.post.featured_local; const labelLocal = featuredLocal
const label_local = featured_local
? i18n.t("unfeature_from_local") ? i18n.t("unfeature_from_local")
: i18n.t("feature_in_local"); : i18n.t("feature_in_local");
return ( return (
<span> <span>
<button <button
className="btn btn-link btn-animate text-muted py-0 pl-0" className="btn btn-link btn-animate text-muted py-0 pl-0"
onClick={() => this.handleModFeaturePost(this, true)} onClick={linkEvent(this, this.handleModFeaturePostCommunity)}
data-tippy-content={label_community} data-tippy-content={labelCommunity}
aria-label={label_community} aria-label={labelCommunity}
> >
<Icon <Icon
icon="pin" icon="pin"
classes={classNames({ "text-success": featured_community })} classes={classNames({ "text-success": featuredCommunity })}
inline inline
/>{" "} />{" "}
Community Community
</button> </button>
{is_admin && ( {amAdmin() && (
<button <button
className="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={() => this.handleModFeaturePost(this, false)} onClick={linkEvent(this, this.handleModFeaturePostLocal)}
data-tippy-content={label_local} data-tippy-content={labelLocal}
aria-label={label_local} aria-label={labelLocal}
> >
<Icon <Icon
icon="pin" icon="pin"
classes={classNames({ "text-success": featured_local })} classes={classNames({ "text-success": featuredLocal })}
inline inline
/>{" "} />{" "}
Local Local
@ -1527,20 +1534,26 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
} }
handleModFeaturePost(i: PostListing, is_community: boolean) { handleModFeaturePostLocal(i: PostListing) {
let auth = myAuth(); let auth = myAuth();
if (auth) { if (auth) {
let featured: [PostFeatureType, boolean] = is_community
? [
PostFeatureType.Community,
!i.props.post_view.post.featured_community,
]
: [PostFeatureType.Local, !i.props.post_view.post.featured_local];
let form: FeaturePost = { let form: FeaturePost = {
post_id: i.props.post_view.post.id, post_id: i.props.post_view.post.id,
feature_type: featured[0], feature_type: PostFeatureType.Local,
featured: featured[1], featured: !i.props.post_view.post.featured_local,
auth,
};
WebSocketService.Instance.send(wsClient.featurePost(form));
}
}
handleModFeaturePostCommunity(i: PostListing) {
let auth = myAuth();
if (auth) {
let form: FeaturePost = {
post_id: i.props.post_view.post.id,
feature_type: PostFeatureType.Community,
featured: !i.props.post_view.post.featured_community,
auth, auth,
}; };
WebSocketService.Instance.send(wsClient.featurePost(form)); WebSocketService.Instance.send(wsClient.featurePost(form));

View file

@ -651,6 +651,7 @@ export class Post extends Component<any, PostState> {
data.recipient_ids.length == 0 && data.recipient_ids.length == 0 &&
!creatorBlocked && !creatorBlocked &&
postRes && postRes &&
data.comment_view.post.id == postRes.post_view.post.id &&
commentsRes commentsRes
) { ) {
commentsRes.comments.unshift(data.comment_view); commentsRes.comments.unshift(data.comment_view);

View file

@ -75,7 +75,7 @@ if (isBrowser()) {
} }
interface SearchProps { interface SearchProps {
q: string; q?: string;
type_: SearchType; type_: SearchType;
sort: SortType; sort: SortType;
listingType: ListingType; listingType: ListingType;
@ -85,7 +85,7 @@ interface SearchProps {
} }
interface SearchState { interface SearchState {
q: string; q?: string;
type_: SearchType; type_: SearchType;
sort: SortType; sort: SortType;
listingType: ListingType; listingType: ListingType;
@ -97,7 +97,7 @@ interface SearchState {
creatorDetails?: GetPersonDetailsResponse; creatorDetails?: GetPersonDetailsResponse;
loading: boolean; loading: boolean;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
searchText: string; searchText?: string;
resolveObjectResponse?: ResolveObjectResponse; resolveObjectResponse?: ResolveObjectResponse;
} }
@ -135,13 +135,13 @@ export class Search extends Component<any, SearchState> {
this.props.match.params.community_id this.props.match.params.community_id
), ),
creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id), creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id),
loading: true, loading: false,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
communities: [], communities: [],
}; };
static getSearchQueryFromProps(q: string): string { static getSearchQueryFromProps(q?: string): string | undefined {
return decodeURIComponent(q) || ""; return q ? decodeURIComponent(q) : undefined;
} }
static getSearchTypeFromProps(type_: string): SearchType { static getSearchTypeFromProps(type_: string): SearchType {
@ -219,7 +219,10 @@ export class Search extends Component<any, SearchState> {
} }
} else { } else {
this.fetchCommunities(); this.fetchCommunities();
this.search();
if (this.state.q) {
this.search();
}
} }
} }
@ -298,29 +301,33 @@ export class Search extends Component<any, SearchState> {
promises.push(Promise.resolve()); promises.push(Promise.resolve());
} }
let form: SearchForm = { let q = this.getSearchQueryFromProps(pathSplit[3]);
q: this.getSearchQueryFromProps(pathSplit[3]),
community_id,
creator_id,
type_: this.getSearchTypeFromProps(pathSplit[5]),
sort: this.getSortTypeFromProps(pathSplit[7]),
listing_type: this.getListingTypeFromProps(pathSplit[9]),
page: this.getPageFromProps(pathSplit[15]),
limit: fetchLimit,
auth: req.auth,
};
let resolveObjectForm: ResolveObject = { if (q) {
q: this.getSearchQueryFromProps(pathSplit[3]), let form: SearchForm = {
auth: req.auth, q,
}; community_id,
creator_id,
type_: this.getSearchTypeFromProps(pathSplit[5]),
sort: this.getSortTypeFromProps(pathSplit[7]),
listing_type: this.getListingTypeFromProps(pathSplit[9]),
page: this.getPageFromProps(pathSplit[15]),
limit: fetchLimit,
auth: req.auth,
};
if (form.q != "") { let resolveObjectForm: ResolveObject = {
promises.push(req.client.search(form)); q,
promises.push(req.client.resolveObject(resolveObjectForm)); auth: req.auth,
} else { };
promises.push(Promise.resolve());
promises.push(Promise.resolve()); if (form.q != "") {
promises.push(req.client.search(form));
promises.push(req.client.resolveObject(resolveObjectForm));
} else {
promises.push(Promise.resolve());
promises.push(Promise.resolve());
}
} }
return promises; return promises;
@ -336,11 +343,13 @@ export class Search extends Component<any, SearchState> {
lastState.creatorId !== this.state.creatorId || lastState.creatorId !== this.state.creatorId ||
lastState.page !== this.state.page lastState.page !== this.state.page
) { ) {
this.setState({ if (this.state.q) {
loading: true, this.setState({
searchText: this.state.q, loading: true,
}); searchText: this.state.q,
this.search(); });
this.search();
}
} }
} }
@ -779,24 +788,24 @@ export class Search extends Component<any, SearchState> {
this.state.creatorId == 0 ? undefined : this.state.creatorId; this.state.creatorId == 0 ? undefined : this.state.creatorId;
let auth = myAuth(false); let auth = myAuth(false);
let form: SearchForm = { if (this.state.q && this.state.q != "") {
q: this.state.q, let form: SearchForm = {
community_id, q: this.state.q,
creator_id, community_id,
type_: this.state.type_, creator_id,
sort: this.state.sort, type_: this.state.type_,
listing_type: this.state.listingType, sort: this.state.sort,
page: this.state.page, listing_type: this.state.listingType,
limit: fetchLimit, page: this.state.page,
auth, limit: fetchLimit,
}; auth,
};
let resolveObjectForm: ResolveObject = { let resolveObjectForm: ResolveObject = {
q: this.state.q, q: this.state.q,
auth, auth,
}; };
if (this.state.q != "") {
this.setState({ this.setState({
searchResponse: undefined, searchResponse: undefined,
resolveObjectResponse: undefined, resolveObjectResponse: undefined,
@ -919,7 +928,7 @@ export class Search extends Component<any, SearchState> {
updateUrl(paramUpdates: UrlParams) { updateUrl(paramUpdates: UrlParams) {
const qStr = paramUpdates.q || this.state.q; const qStr = paramUpdates.q || this.state.q;
const qStrEncoded = encodeURIComponent(qStr); const qStrEncoded = encodeURIComponent(qStr || "");
const typeStr = paramUpdates.type_ || this.state.type_; const typeStr = paramUpdates.type_ || this.state.type_;
const listingTypeStr = paramUpdates.listingType || this.state.listingType; const listingTypeStr = paramUpdates.listingType || this.state.listingType;
const sortStr = paramUpdates.sort || this.state.sort; const sortStr = paramUpdates.sort || this.state.sort;