pl-fe: allow setting location for posts if supported by backend
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -18,13 +18,7 @@ import { tagSchema } from './tag';
|
||||
import { translationSchema } from './translation';
|
||||
import { datetimeSchema, filteredArray } from './utils';
|
||||
|
||||
const statusEventSchema = v.object({
|
||||
name: v.fallback(v.string(), ''),
|
||||
start_time: v.fallback(v.nullable(datetimeSchema), null),
|
||||
end_time: v.fallback(v.nullable(datetimeSchema), null),
|
||||
join_mode: v.fallback(v.nullable(v.picklist(['free', 'restricted', 'invite', 'external'])), null),
|
||||
participants_count: v.fallback(v.number(), 0),
|
||||
location: v.fallback(v.nullable(v.object({
|
||||
const locationSchema = v.object({
|
||||
name: v.fallback(v.string(), ''),
|
||||
url: v.fallback(v.pipe(v.string(), v.url()), ''),
|
||||
latitude: v.fallback(v.nullable(v.number()), null),
|
||||
@ -34,7 +28,15 @@ const statusEventSchema = v.object({
|
||||
locality: v.fallback(v.string(), ''),
|
||||
region: v.fallback(v.string(), ''),
|
||||
country: v.fallback(v.string(), ''),
|
||||
})), null),
|
||||
});
|
||||
|
||||
const statusEventSchema = v.object({
|
||||
name: v.fallback(v.string(), ''),
|
||||
start_time: v.fallback(v.nullable(datetimeSchema), null),
|
||||
end_time: v.fallback(v.nullable(datetimeSchema), null),
|
||||
join_mode: v.fallback(v.nullable(v.picklist(['free', 'restricted', 'invite', 'external'])), null),
|
||||
participants_count: v.fallback(v.number(), 0),
|
||||
location: v.fallback(v.nullable(locationSchema), null),
|
||||
join_state: v.fallback(v.nullable(v.picklist(['pending', 'reject', 'accept'])), null),
|
||||
});
|
||||
|
||||
@ -112,6 +114,7 @@ const baseStatusSchema = v.object({
|
||||
interaction_policy: interactionPolicySchema,
|
||||
|
||||
content_type: v.fallback(v.nullable(v.string()), null),
|
||||
location: v.fallback(v.nullable(locationSchema), null),
|
||||
});
|
||||
|
||||
const preprocess = (status: any) => {
|
||||
@ -166,6 +169,7 @@ const preprocess = (status: any) => {
|
||||
'event',
|
||||
'translation',
|
||||
'rss_feed',
|
||||
'location',
|
||||
])),
|
||||
...(pick(status.friendica || {}, [
|
||||
'dislikes_count',
|
||||
|
||||
@ -1715,6 +1715,8 @@ const getFeatures = (instance: Instance) => {
|
||||
*/
|
||||
statusDislikes: v.software === FRIENDICA && gte(v.version, '2023.3.0'),
|
||||
|
||||
statusLocation: instance.api_versions['status_location.pleroma.pl-api'] >= 1,
|
||||
|
||||
/**
|
||||
* @see GET /api/web/stories/v1/recent
|
||||
* @see GET /api/web/stories/v1/viewers
|
||||
|
||||
@ -97,6 +97,8 @@ interface CreateStatusOptionalParams {
|
||||
*/
|
||||
quote_approval_policy?: 'public' | 'followers' | 'nobody';
|
||||
|
||||
location_id?: string;
|
||||
|
||||
/**
|
||||
* If set to true, this status will be "local only" and will NOT be federated beyond the local timeline(s). If set to false (default), this status will be federated to your followers beyond the local timeline(s).
|
||||
*/
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pl-api",
|
||||
"version": "1.0.0-rc.96",
|
||||
"version": "1.0.0-rc.97",
|
||||
"type": "module",
|
||||
"homepage": "https://codeberg.org/mkljczk/pl-fe/src/branch/develop/packages/pl-api",
|
||||
"repository": {
|
||||
|
||||
@ -20,7 +20,7 @@ import { createStatus } from './statuses';
|
||||
|
||||
import type { LinkOptions } from '@tanstack/react-router';
|
||||
import type { EditorState } from 'lexical';
|
||||
import type { Account, CreateStatusParams, CustomEmoji, Group, MediaAttachment, Status as BaseStatus, Tag, Poll, ScheduledStatus, InteractionPolicy, UpdateMediaParams } from 'pl-api';
|
||||
import type { Account, CreateStatusParams, CustomEmoji, Group, MediaAttachment, Status as BaseStatus, Tag, Poll, ScheduledStatus, InteractionPolicy, UpdateMediaParams, Location } from 'pl-api';
|
||||
import type { AutoSuggestion } from 'pl-fe/components/autosuggest-input';
|
||||
import type { Emoji } from 'pl-fe/features/emoji';
|
||||
import type { Status } from 'pl-fe/normalizers/status';
|
||||
@ -104,6 +104,9 @@ const COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE = 'COMPOSE_HASHTAG_CASING_SUGGEST
|
||||
|
||||
const COMPOSE_REDACTING_OVERWRITE_CHANGE = 'COMPOSE_REDACTING_OVERWRITE_CHANGE' as const;
|
||||
|
||||
const COMPOSE_SET_LOCATION = 'COMPOSE_SET_LOCATION' as const;
|
||||
const COMPOSE_SET_SHOW_LOCATION_PICKER = 'COMPOSE_SET_SHOW_LOCATION_PICKER' as const;
|
||||
|
||||
const messages = defineMessages({
|
||||
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
|
||||
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent!' },
|
||||
@ -419,6 +422,7 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}, preview
|
||||
local_only: compose.localOnly,
|
||||
interaction_policy: ['public', 'unlisted', 'private'].includes(compose.visibility) && compose.interactionPolicy || undefined,
|
||||
quote_approval_policy: compose.quoteApprovalPolicy || undefined,
|
||||
location_id: compose.location?.origin_id || undefined,
|
||||
preview,
|
||||
};
|
||||
|
||||
@ -989,6 +993,18 @@ const changeComposeRedactingOverwrite = (composeId: string, value: boolean) => (
|
||||
value,
|
||||
});
|
||||
|
||||
const setComposeLocation = (composeId: string, location: Location | null) => ({
|
||||
type: COMPOSE_SET_LOCATION,
|
||||
composeId,
|
||||
location,
|
||||
});
|
||||
|
||||
const setComposeShowLocationPicker = (composeId: string, showLocation: boolean) => ({
|
||||
type: COMPOSE_SET_SHOW_LOCATION_PICKER,
|
||||
composeId,
|
||||
showLocation,
|
||||
});
|
||||
|
||||
type ComposeAction =
|
||||
ComposeSetStatusAction
|
||||
| ReturnType<typeof changeCompose>
|
||||
@ -1048,7 +1064,9 @@ type ComposeAction =
|
||||
| ReturnType<typeof ignoreClearLinkSuggestion>
|
||||
| ReturnType<typeof suggestHashtagCasing>
|
||||
| ReturnType<typeof ignoreHashtagCasingSuggestion>
|
||||
| ReturnType<typeof changeComposeRedactingOverwrite>;
|
||||
| ReturnType<typeof changeComposeRedactingOverwrite>
|
||||
| ReturnType<typeof setComposeLocation>
|
||||
| ReturnType<typeof setComposeShowLocationPicker>;
|
||||
|
||||
export {
|
||||
COMPOSE_CHANGE,
|
||||
@ -1110,6 +1128,8 @@ export {
|
||||
COMPOSE_HASHTAG_CASING_SUGGESTION_SET,
|
||||
COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE,
|
||||
COMPOSE_REDACTING_OVERWRITE_CHANGE,
|
||||
COMPOSE_SET_LOCATION,
|
||||
COMPOSE_SET_SHOW_LOCATION_PICKER,
|
||||
setComposeToStatus,
|
||||
replyCompose,
|
||||
cancelReplyCompose,
|
||||
@ -1163,6 +1183,8 @@ export {
|
||||
suggestHashtagCasing,
|
||||
ignoreHashtagCasingSuggestion,
|
||||
changeComposeRedactingOverwrite,
|
||||
setComposeLocation,
|
||||
setComposeShowLocationPicker,
|
||||
type ComposeReplyAction,
|
||||
type ComposeSuggestionSelectAction,
|
||||
type ComposeAction,
|
||||
|
||||
@ -45,6 +45,8 @@ import DriveButton from './drive-button';
|
||||
import HashtagCasingSuggestion from './hashtag-casing-suggestion';
|
||||
import InteractionPolicyButton from './interaction-policy-button';
|
||||
import LanguageDropdown from './language-dropdown';
|
||||
import LocationButton from './location-button';
|
||||
import LocationForm from './location-form';
|
||||
import PollButton from './poll-button';
|
||||
import PollForm from './polls/poll-form';
|
||||
import PrivacyDropdown from './privacy-dropdown';
|
||||
@ -288,16 +290,18 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
{features.scheduledStatuses && <ScheduleButton composeId={id} />}
|
||||
{anyMedia && features.spoilers && <SensitiveMediaButton composeId={id} />}
|
||||
{(features.interactionRequests || features.quoteApprovalPolicies) && <InteractionPolicyButton composeId={id} />}
|
||||
{features.statusLocation && <LocationButton composeId={id} />}
|
||||
</div>
|
||||
), [features, id, anyMedia]);
|
||||
|
||||
const showModifiers = !condensed && (compose.mediaAttachments.length || compose.isUploading || compose.poll?.options.length || compose.scheduledAt);
|
||||
const showModifiers = !condensed && (compose.mediaAttachments.length || compose.isUploading || compose.poll?.options.length || compose.scheduledAt || compose.showLocationPicker);
|
||||
|
||||
const composeModifiers = showModifiers && (
|
||||
<div className='⁂-compose-form__modifiers'>
|
||||
<UploadForm composeId={id} onSubmit={handleSubmit} />
|
||||
<PollForm composeId={id} />
|
||||
<ScheduleForm composeId={id} />
|
||||
<LocationForm composeId={id} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { setComposeShowLocationPicker } from 'pl-fe/actions/compose';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useCompose } from 'pl-fe/hooks/use-compose';
|
||||
|
||||
import ComposeFormButton from './compose-form-button';
|
||||
|
||||
const messages = defineMessages({
|
||||
show_location_picker: { id: 'location_button.show_location_picker', defaultMessage: 'Show location picker' },
|
||||
hide_location_picker: { id: 'location_button.hide_location_picker', defaultMessage: 'Hide location picker' },
|
||||
});
|
||||
|
||||
interface ILocationButton {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
const LocationButton: React.FC<ILocationButton> = ({ composeId }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const compose = useCompose(composeId);
|
||||
|
||||
const unavailable = compose.isUploading;
|
||||
const active = compose.showLocationPicker;
|
||||
|
||||
const onClick = () => {
|
||||
if (active) {
|
||||
dispatch(setComposeShowLocationPicker(composeId, false));
|
||||
} else {
|
||||
dispatch(setComposeShowLocationPicker(composeId, true));
|
||||
}
|
||||
};
|
||||
|
||||
if (unavailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ComposeFormButton
|
||||
icon={require('@phosphor-icons/core/regular/map-pin.svg')}
|
||||
title={intl.formatMessage(active ? messages.hide_location_picker : messages.show_location_picker)}
|
||||
active={active}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { LocationButton as default };
|
||||
@ -0,0 +1,58 @@
|
||||
import { Location } from 'pl-api';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { setComposeLocation } from 'pl-fe/actions/compose';
|
||||
import { ADDRESS_ICONS } from 'pl-fe/components/autosuggest-location';
|
||||
import LocationSearch from 'pl-fe/components/location-search';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import IconButton from 'pl-fe/components/ui/icon-button';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useCompose } from 'pl-fe/hooks/use-compose';
|
||||
|
||||
const messages = defineMessages({
|
||||
resetLocation: { id: 'compose_event.reset_location', defaultMessage: 'Reset location' },
|
||||
});
|
||||
|
||||
interface ILocationForm {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
const LocationForm: React.FC<ILocationForm> = ({ composeId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const { showLocationPicker, location } = useCompose(composeId);
|
||||
|
||||
const onChangeLocation = (location: Location | null) => {
|
||||
dispatch(setComposeLocation(composeId, location));
|
||||
};
|
||||
|
||||
if (!showLocationPicker) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='⁂-compose-form__schedule'>
|
||||
{location ? (
|
||||
<HStack className='h-[38px] text-gray-700 dark:text-gray-500' alignItems='center' space={2}>
|
||||
<Icon src={ADDRESS_ICONS[location.type] || require('@phosphor-icons/core/regular/map-pin.svg')} />
|
||||
<Stack className='grow'>
|
||||
<Text>{location.description}</Text>
|
||||
<Text theme='muted' size='xs'>{[location.street, location.locality, location.country].filter(val => val?.trim()).join(' · ')}</Text>
|
||||
</Stack>
|
||||
<IconButton title={intl.formatMessage(messages.resetLocation)} src={require('@phosphor-icons/core/regular/x.svg')} onClick={() => onChangeLocation(null)} />
|
||||
</HStack>
|
||||
) : (
|
||||
<LocationSearch
|
||||
onSelected={onChangeLocation}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { LocationForm as default };
|
||||
@ -60,10 +60,12 @@ import {
|
||||
COMPOSE_PREVIEW_CANCEL,
|
||||
COMPOSE_HASHTAG_CASING_SUGGESTION_SET,
|
||||
COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE,
|
||||
type ComposeAction,
|
||||
type ComposeSuggestionSelectAction,
|
||||
COMPOSE_REDACTING_OVERWRITE_CHANGE,
|
||||
COMPOSE_QUOTE_POLICY_OPTION_CHANGE,
|
||||
COMPOSE_SET_LOCATION,
|
||||
COMPOSE_SET_SHOW_LOCATION_PICKER,
|
||||
type ComposeAction,
|
||||
type ComposeSuggestionSelectAction,
|
||||
} from '../actions/compose';
|
||||
import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events';
|
||||
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, type MeAction } from '../actions/me';
|
||||
@ -71,7 +73,7 @@ import { FE_NAME } from '../actions/settings';
|
||||
import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines';
|
||||
import { unescapeHTML } from '../utils/html';
|
||||
|
||||
import type { Account, CredentialAccount, Instance, InteractionPolicy, MediaAttachment, Status as BaseStatus, Tag, CreateStatusParams } from 'pl-api';
|
||||
import type { Account, CredentialAccount, Instance, InteractionPolicy, Location, MediaAttachment, Status as BaseStatus, Tag, CreateStatusParams } from 'pl-api';
|
||||
import type { Emoji } from 'pl-fe/features/emoji';
|
||||
import type { Language } from 'pl-fe/features/preferences';
|
||||
import type { Status } from 'pl-fe/normalizers/status';
|
||||
@ -113,6 +115,7 @@ interface Compose {
|
||||
// Non-text content
|
||||
mediaAttachments: Array<MediaAttachment>;
|
||||
poll: ComposePoll | null;
|
||||
location: Location | null;
|
||||
|
||||
// Post settings
|
||||
contentType: string;
|
||||
@ -157,6 +160,7 @@ interface Compose {
|
||||
preview: Partial<BaseStatus> | null;
|
||||
suggestedLanguage: string | null;
|
||||
suggestions: Array<string> | Array<Emoji>;
|
||||
showLocationPicker: boolean;
|
||||
|
||||
// Moderation features
|
||||
redacting: boolean;
|
||||
@ -173,6 +177,7 @@ const newCompose = (params: Partial<Compose> = {}): Compose => ({
|
||||
|
||||
mediaAttachments: [],
|
||||
poll: null,
|
||||
location: null,
|
||||
|
||||
contentType: 'text/plain',
|
||||
interactionPolicy: null,
|
||||
@ -211,6 +216,7 @@ const newCompose = (params: Partial<Compose> = {}): Compose => ({
|
||||
preview: null,
|
||||
suggestedLanguage: null,
|
||||
suggestions: [],
|
||||
showLocationPicker: false,
|
||||
|
||||
redacting: false,
|
||||
redactingOverwrite: false,
|
||||
@ -753,6 +759,17 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
compose.redactingOverwrite = action.value;
|
||||
});
|
||||
case COMPOSE_SET_LOCATION:
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
compose.location = action.location;
|
||||
});
|
||||
case COMPOSE_SET_SHOW_LOCATION_PICKER:
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
compose.showLocationPicker = action.showLocation;
|
||||
if (!action.showLocation) {
|
||||
compose.location = null;
|
||||
}
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user