Merge remote-tracking branch 'overseerr/develop' into develop

This commit is contained in:
notfakie
2022-09-01 18:11:15 +12:00
473 changed files with 15548 additions and 8433 deletions

View File

@@ -1,9 +1,9 @@
import Button from '@app/components/Common/Button';
import type { User } from '@app/hooks/useUser';
import { Permission, useUser } from '@app/hooks/useUser';
import { CogIcon, UserIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Permission, User, useUser } from '../../../hooks/useUser';
import Button from '../../Common/Button';
const messages = defineMessages({
settings: 'Edit Settings',
@@ -17,10 +17,7 @@ interface ProfileHeaderProps {
isSettingsPage?: boolean;
}
const ProfileHeader: React.FC<ProfileHeaderProps> = ({
user,
isSettingsPage,
}) => {
const ProfileHeader = ({ user, isSettingsPage }: ProfileHeaderProps) => {
const intl = useIntl();
const { user: loggedInUser, hasPermission } = useUser();
@@ -61,7 +58,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
user.id === loggedInUser?.id ? '/profile' : `/users/${user.id}`
}
>
<a className="bg-gradient-to-br from-indigo-400 to-purple-400 bg-clip-text text-lg font-bold text-transparent hover:to-purple-200 sm:text-2xl">
<a className="text-overseerr text-lg font-bold hover:to-purple-200 sm:text-2xl">
{user.displayName}
</a>
</Link>

View File

@@ -1,30 +1,28 @@
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import LanguageSelector from '@app/components/LanguageSelector';
import QuotaSelector from '@app/components/QuotaSelector';
import RegionSelector from '@app/components/RegionSelector';
import type { AvailableLocale } from '@app/context/LanguageContext';
import { availableLanguages } from '@app/context/LanguageContext';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import { Permission, UserType, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import { SaveIcon } from '@heroicons/react/outline';
import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { UserSettingsGeneralResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import {
availableLanguages,
AvailableLocale,
} from '../../../../context/LanguageContext';
import useLocale from '../../../../hooks/useLocale';
import useSettings from '../../../../hooks/useSettings';
import { Permission, UserType, useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Error from '../../../../pages/_error';
import Badge from '../../../Common/Badge';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import PageTitle from '../../../Common/PageTitle';
import LanguageSelector from '../../../LanguageSelector';
import QuotaSelector from '../../../QuotaSelector';
import RegionSelector from '../../../RegionSelector';
import getConfig from 'next/config';
const messages = defineMessages({
general: 'General',
@@ -56,9 +54,15 @@ const messages = defineMessages({
discordIdTip:
'The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your Discord user account',
validationDiscordId: 'You must provide a valid Discord user ID',
plexwatchlistsyncmovies: 'Auto-Request Movies',
plexwatchlistsyncmoviestip:
'Automatically request movies on your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink>',
plexwatchlistsyncseries: 'Auto-Request Series',
plexwatchlistsyncseriestip:
'Automatically request series on your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink>',
});
const UserGeneralSettings: React.FC = () => {
const UserGeneralSettings = () => {
const intl = useIntl();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts();
@@ -86,7 +90,7 @@ const UserGeneralSettings: React.FC = () => {
const UserGeneralSettingsSchema = Yup.object().shape({
discordId: Yup.string()
.nullable()
.matches(/^\d{17,18}$/, intl.formatMessage(messages.validationDiscordId)),
.matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)),
});
useEffect(() => {
@@ -131,6 +135,8 @@ const UserGeneralSettings: React.FC = () => {
movieQuotaDays: data?.movieQuotaDays,
tvQuotaLimit: data?.tvQuotaLimit,
tvQuotaDays: data?.tvQuotaDays,
watchlistSyncMovies: data?.watchlistSyncMovies,
watchlistSyncTv: data?.watchlistSyncTv,
}}
validationSchema={UserGeneralSettingsSchema}
enableReinitialize
@@ -149,6 +155,8 @@ const UserGeneralSettings: React.FC = () => {
movieQuotaDays: movieQuotaEnabled ? values.movieQuotaDays : null,
tvQuotaLimit: tvQuotaEnabled ? values.tvQuotaLimit : null,
tvQuotaDays: tvQuotaEnabled ? values.tvQuotaDays : null,
watchlistSyncMovies: values.watchlistSyncMovies,
watchlistSyncTv: values.watchlistSyncTv,
});
if (currentUser?.id === user?.id && setLocale) {
@@ -243,9 +251,11 @@ const UserGeneralSettings: React.FC = () => {
}
/>
</div>
{errors.displayName && touched.displayName && (
<div className="error">{errors.displayName}</div>
)}
{errors.displayName &&
touched.displayName &&
typeof errors.displayName === 'string' && (
<div className="error">{errors.displayName}</div>
)}
</div>
</div>
<div className="form-row">
@@ -280,17 +290,15 @@ const UserGeneralSettings: React.FC = () => {
{currentUser?.id === user?.id && (
<span className="label-tip">
{intl.formatMessage(messages.discordIdTip, {
FindDiscordIdLink: function FindDiscordIdLink(msg) {
return (
<a
href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
FindDiscordIdLink: (msg: React.ReactNode) => (
<a
href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
</span>
)}
@@ -299,9 +307,11 @@ const UserGeneralSettings: React.FC = () => {
<div className="form-input-field">
<Field id="discordId" name="discordId" type="text" />
</div>
{errors.discordId && touched.discordId && (
<div className="error">{errors.discordId}</div>
)}
{errors.discordId &&
touched.discordId &&
typeof errors.discordId === 'string' && (
<div className="error">{errors.discordId}</div>
)}
</div>
</div>
<div className="form-row">
@@ -455,6 +465,99 @@ const UserGeneralSettings: React.FC = () => {
</div>
</>
)}
{hasPermission(
[Permission.AUTO_REQUEST, Permission.AUTO_REQUEST_MOVIE],
{ type: 'or' }
) &&
user?.userType === UserType.PLEX && (
<div className="form-row">
<label
htmlFor="watchlistSyncMovies"
className="checkbox-label"
>
<span>
{intl.formatMessage(messages.plexwatchlistsyncmovies)}
</span>
<span className="label-tip">
{intl.formatMessage(
messages.plexwatchlistsyncmoviestip,
{
PlexWatchlistSupportLink: (
msg: React.ReactNode
) => (
<a
href="https://support.plex.tv/articles/universal-watchlist/"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
}
)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="watchlistSyncMovies"
name="watchlistSyncMovies"
onChange={() => {
setFieldValue(
'watchlistSyncMovies',
!values.watchlistSyncMovies
);
}}
/>
</div>
</div>
)}
{hasPermission(
[Permission.AUTO_REQUEST, Permission.AUTO_REQUEST_TV],
{ type: 'or' }
) &&
user?.userType === UserType.PLEX && (
<div className="form-row">
<label htmlFor="watchlistSyncTv" className="checkbox-label">
<span>
{intl.formatMessage(messages.plexwatchlistsyncseries)}
</span>
<span className="label-tip">
{intl.formatMessage(
messages.plexwatchlistsyncseriestip,
{
PlexWatchlistSupportLink: (
msg: React.ReactNode
) => (
<a
href="https://support.plex.tv/articles/universal-watchlist/"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
}
)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="watchlistSyncTv"
name="watchlistSyncTv"
onChange={() => {
setFieldValue(
'watchlistSyncTv',
!values.watchlistSyncTv
);
}}
/>
</div>
</div>
)}
<div className="actions">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">

View File

@@ -1,18 +1,17 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { SaveIcon } from '@heroicons/react/outline';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
discordsettingssaved: 'Discord notification settings saved successfully!',
@@ -23,7 +22,7 @@ const messages = defineMessages({
validationDiscordId: 'You must provide a valid user ID',
});
const UserNotificationsDiscord: React.FC = () => {
const UserNotificationsDiscord = () => {
const intl = useIntl();
const { addToast } = useToasts();
const router = useRouter();
@@ -46,7 +45,7 @@ const UserNotificationsDiscord: React.FC = () => {
.required(intl.formatMessage(messages.validationDiscordId)),
otherwise: Yup.string().nullable(),
})
.matches(/^\d{17,18}$/, intl.formatMessage(messages.validationDiscordId)),
.matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)),
});
if (!data && !error) {
@@ -111,17 +110,15 @@ const UserNotificationsDiscord: React.FC = () => {
{currentUser?.id === user?.id && (
<span className="label-tip">
{intl.formatMessage(messages.discordIdTip, {
FindDiscordIdLink: function FindDiscordIdLink(msg) {
return (
<a
href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
FindDiscordIdLink: (msg: React.ReactNode) => (
<a
href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
</span>
)}
@@ -130,9 +127,11 @@ const UserNotificationsDiscord: React.FC = () => {
<div className="form-input-field">
<Field id="discordId" name="discordId" type="text" />
</div>
{errors.discordId && touched.discordId && (
<div className="error">{errors.discordId}</div>
)}
{errors.discordId &&
touched.discordId &&
typeof errors.discordId === 'string' && (
<div className="error">{errors.discordId}</div>
)}
</div>
</div>
<NotificationTypeSelector

View File

@@ -1,23 +1,22 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import NotificationTypeSelector, {
ALL_NOTIFICATIONS,
} from '@app/components/NotificationTypeSelector';
import { OpenPgpLink } from '@app/components/Settings/Notifications/NotificationsEmail';
import SettingsBadge from '@app/components/Settings/SettingsBadge';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { SaveIcon } from '@heroicons/react/outline';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Badge from '../../../Common/Badge';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import SensitiveInput from '../../../Common/SensitiveInput';
import NotificationTypeSelector, {
ALL_NOTIFICATIONS,
} from '../../../NotificationTypeSelector';
import { OpenPgpLink } from '../../../Settings/Notifications/NotificationsEmail';
const messages = defineMessages({
emailsettingssaved: 'Email notification settings saved successfully!',
@@ -28,7 +27,7 @@ const messages = defineMessages({
validationPgpPublicKey: 'You must provide a valid PGP public key',
});
const UserEmailSettings: React.FC = () => {
const UserEmailSettings = () => {
const intl = useIntl();
const { addToast } = useToasts();
const router = useRouter();
@@ -106,9 +105,7 @@ const UserEmailSettings: React.FC = () => {
<span className="mr-2">
{intl.formatMessage(messages.pgpPublicKey)}
</span>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.advanced)}
</Badge>
<SettingsBadge badgeType="advanced" />
<span className="label-tip">
{intl.formatMessage(messages.pgpPublicKeyTip, {
OpenPgpLink: OpenPgpLink,
@@ -126,9 +123,11 @@ const UserEmailSettings: React.FC = () => {
className="font-mono text-xs"
/>
</div>
{errors.pgpKey && touched.pgpKey && (
<div className="error">{errors.pgpKey}</div>
)}
{errors.pgpKey &&
touched.pgpKey &&
typeof errors.pgpKey === 'string' && (
<div className="error">{errors.pgpKey}</div>
)}
</div>
</div>
<NotificationTypeSelector

View File

@@ -1,18 +1,17 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import SensitiveInput from '../../../Common/SensitiveInput';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
pushbulletsettingssaved:
@@ -24,7 +23,7 @@ const messages = defineMessages({
validationPushbulletAccessToken: 'You must provide an access token',
});
const UserPushbulletSettings: React.FC = () => {
const UserPushbulletSettings = () => {
const intl = useIntl();
const { addToast } = useToasts();
const router = useRouter();
@@ -105,20 +104,16 @@ const UserPushbulletSettings: React.FC = () => {
{data?.pushbulletAccessToken && (
<span className="label-tip">
{intl.formatMessage(messages.pushbulletAccessTokenTip, {
PushbulletSettingsLink: function PushbulletSettingsLink(
msg
) {
return (
<a
href="https://www.pushbullet.com/#settings/account"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
PushbulletSettingsLink: (msg: React.ReactNode) => (
<a
href="https://www.pushbullet.com/#settings/account"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
</span>
)}

View File

@@ -1,18 +1,17 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import useSettings from '@app/hooks/useSettings';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import useSettings from '../../../../hooks/useSettings';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
pushoversettingssaved: 'Pushover notification settings saved successfully!',
@@ -28,7 +27,7 @@ const messages = defineMessages({
validationPushoverUserKey: 'You must provide a valid user or group key',
});
const UserPushoverSettings: React.FC = () => {
const UserPushoverSettings = () => {
const intl = useIntl();
const settings = useSettings();
const { addToast } = useToasts();
@@ -129,19 +128,16 @@ const UserPushoverSettings: React.FC = () => {
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.pushoverApplicationTokenTip, {
ApplicationRegistrationLink:
function ApplicationRegistrationLink(msg) {
return (
<a
href="https://pushover.net/api#registration"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
ApplicationRegistrationLink: (msg: React.ReactNode) => (
<a
href="https://pushover.net/api#registration"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
applicationTitle: settings.currentSettings.applicationTitle,
})}
</span>
@@ -167,18 +163,16 @@ const UserPushoverSettings: React.FC = () => {
{intl.formatMessage(messages.pushoverUserKey)}
<span className="label-tip">
{intl.formatMessage(messages.pushoverUserKeyTip, {
UsersGroupsLink: function UsersGroupsLink(msg) {
return (
<a
href="https://pushover.net/api#identifiers"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
UsersGroupsLink: (msg: React.ReactNode) => (
<a
href="https://pushover.net/api#identifiers"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
</span>
</label>
@@ -190,9 +184,11 @@ const UserPushoverSettings: React.FC = () => {
type="text"
/>
</div>
{errors.pushoverUserKey && touched.pushoverUserKey && (
<div className="error">{errors.pushoverUserKey}</div>
)}
{errors.pushoverUserKey &&
touched.pushoverUserKey &&
typeof errors.pushoverUserKey === 'string' && (
<div className="error">{errors.pushoverUserKey}</div>
)}
</div>
</div>
<NotificationTypeSelector

View File

@@ -1,18 +1,17 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { SaveIcon } from '@heroicons/react/outline';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
telegramsettingssaved: 'Telegram notification settings saved successfully!',
@@ -25,7 +24,7 @@ const messages = defineMessages({
validationTelegramChatId: 'You must provide a valid chat ID',
});
const UserTelegramSettings: React.FC = () => {
const UserTelegramSettings = () => {
const intl = useIntl();
const { addToast } = useToasts();
const router = useRouter();
@@ -112,31 +111,25 @@ const UserTelegramSettings: React.FC = () => {
{data?.telegramBotUsername && (
<span className="label-tip">
{intl.formatMessage(messages.telegramChatIdTipLong, {
TelegramBotLink: function TelegramBotLink(msg) {
return (
<a
href={`https://telegram.me/${data.telegramBotUsername}`}
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
GetIdBotLink: function GetIdBotLink(msg) {
return (
<a
href="https://telegram.me/get_id_bot"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
code: function code(msg) {
return <code>{msg}</code>;
},
TelegramBotLink: (msg: React.ReactNode) => (
<a
href={`https://telegram.me/${data.telegramBotUsername}`}
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
GetIdBotLink: (msg: React.ReactNode) => (
<a
href="https://telegram.me/get_id_bot"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
code: (msg: React.ReactNode) => <code>{msg}</code>,
})}
</span>
)}
@@ -149,9 +142,11 @@ const UserTelegramSettings: React.FC = () => {
type="text"
/>
</div>
{errors.telegramChatId && touched.telegramChatId && (
<div className="error">{errors.telegramChatId}</div>
)}
{errors.telegramChatId &&
touched.telegramChatId &&
typeof errors.telegramChatId === 'string' && (
<div className="error">{errors.telegramChatId}</div>
)}
</div>
</div>
<div className="form-row">

View File

@@ -1,26 +1,25 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import NotificationTypeSelector, {
ALL_NOTIFICATIONS,
} from '@app/components/NotificationTypeSelector';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { SaveIcon } from '@heroicons/react/outline';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import NotificationTypeSelector, {
ALL_NOTIFICATIONS,
} from '../../../NotificationTypeSelector';
const messages = defineMessages({
webpushsettingssaved: 'Web push notification settings saved successfully!',
webpushsettingsfailed: 'Web push notification settings failed to save.',
});
const UserWebPushSettings: React.FC = () => {
const UserWebPushSettings = () => {
const intl = useIntl();
const { addToast } = useToasts();
const router = useRouter();

View File

@@ -1,19 +1,19 @@
import DiscordLogo from '@app/assets/extlogos/discord.svg';
import PushbulletLogo from '@app/assets/extlogos/pushbullet.svg';
import PushoverLogo from '@app/assets/extlogos/pushover.svg';
import TelegramLogo from '@app/assets/extlogos/telegram.svg';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import type { SettingsRoute } from '@app/components/Common/SettingsTabs';
import SettingsTabs from '@app/components/Common/SettingsTabs';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import { CloudIcon, MailIcon } from '@heroicons/react/solid';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import DiscordLogo from '../../../../assets/extlogos/discord.svg';
import PushbulletLogo from '../../../../assets/extlogos/pushbullet.svg';
import PushoverLogo from '../../../../assets/extlogos/pushover.svg';
import TelegramLogo from '../../../../assets/extlogos/telegram.svg';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Error from '../../../../pages/_error';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import PageTitle from '../../../Common/PageTitle';
import SettingsTabs, { SettingsRoute } from '../../../Common/SettingsTabs';
const messages = defineMessages({
notifications: 'Notifications',
@@ -22,7 +22,13 @@ const messages = defineMessages({
webpush: 'Web Push',
});
const UserNotificationSettings: React.FC = ({ children }) => {
type UserNotificationSettingsProps = {
children: React.ReactNode;
};
const UserNotificationSettings = ({
children,
}: UserNotificationSettingsProps) => {
const intl = useIntl();
const router = useRouter();
const { user } = useUser({ id: Number(router.query.userId) });

View File

@@ -1,20 +1,19 @@
import Alert from '@app/components/Common/Alert';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { Permission, useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Error from '../../../../pages/_error';
import Alert from '../../../Common/Alert';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import PageTitle from '../../../Common/PageTitle';
import SensitiveInput from '../../../Common/SensitiveInput';
const messages = defineMessages({
password: 'Password',
@@ -39,7 +38,7 @@ const messages = defineMessages({
"You do not have permission to modify this user's password.",
});
const UserPasswordChange: React.FC = () => {
const UserPasswordChange = () => {
const intl = useIntl();
const { addToast } = useToasts();
const router = useRouter();
@@ -176,9 +175,11 @@ const UserPasswordChange: React.FC = () => {
autoComplete="current-password"
/>
</div>
{errors.currentPassword && touched.currentPassword && (
<div className="error">{errors.currentPassword}</div>
)}
{errors.currentPassword &&
touched.currentPassword &&
typeof errors.currentPassword === 'string' && (
<div className="error">{errors.currentPassword}</div>
)}
</div>
</div>
)}
@@ -196,9 +197,11 @@ const UserPasswordChange: React.FC = () => {
autoComplete="new-password"
/>
</div>
{errors.newPassword && touched.newPassword && (
<div className="error">{errors.newPassword}</div>
)}
{errors.newPassword &&
touched.newPassword &&
typeof errors.newPassword === 'string' && (
<div className="error">{errors.newPassword}</div>
)}
</div>
</div>
<div className="form-row">
@@ -215,9 +218,11 @@ const UserPasswordChange: React.FC = () => {
autoComplete="new-password"
/>
</div>
{errors.confirmPassword && touched.confirmPassword && (
<div className="error">{errors.confirmPassword}</div>
)}
{errors.confirmPassword &&
touched.confirmPassword &&
typeof errors.confirmPassword === 'string' && (
<div className="error">{errors.confirmPassword}</div>
)}
</div>
</div>
<div className="actions">

View File

@@ -1,19 +1,18 @@
import Alert from '@app/components/Common/Alert';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import PermissionEdit from '@app/components/PermissionEdit';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Error from '../../../../pages/_error';
import Alert from '../../../Common/Alert';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import PageTitle from '../../../Common/PageTitle';
import PermissionEdit from '../../../PermissionEdit';
const messages = defineMessages({
toastSettingsSuccess: 'Permissions saved successfully!',
@@ -22,7 +21,7 @@ const messages = defineMessages({
unauthorizedDescription: 'You cannot modify your own permissions.',
});
const UserPermissions: React.FC = () => {
const UserPermissions = () => {
const intl = useIntl();
const { addToast } = useToasts();
const router = useRouter();

View File

@@ -1,18 +1,18 @@
import Alert from '@app/components/Common/Alert';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import type { SettingsRoute } from '@app/components/Common/SettingsTabs';
import SettingsTabs from '@app/components/Common/SettingsTabs';
import ProfileHeader from '@app/components/UserProfile/ProfileHeader';
import useSettings from '@app/hooks/useSettings';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import { hasPermission, Permission } from '@server/lib/permissions';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { UserSettingsNotificationsResponse } from '../../../../server/interfaces/api/userSettingsInterfaces';
import { hasPermission, Permission } from '../../../../server/lib/permissions';
import useSettings from '../../../hooks/useSettings';
import { useUser } from '../../../hooks/useUser';
import globalMessages from '../../../i18n/globalMessages';
import Error from '../../../pages/_error';
import Alert from '../../Common/Alert';
import LoadingSpinner from '../../Common/LoadingSpinner';
import PageTitle from '../../Common/PageTitle';
import SettingsTabs, { SettingsRoute } from '../../Common/SettingsTabs';
import ProfileHeader from '../ProfileHeader';
const messages = defineMessages({
menuGeneralSettings: 'General',
@@ -23,7 +23,11 @@ const messages = defineMessages({
"You do not have permission to modify this user's settings.",
});
const UserSettings: React.FC = ({ children }) => {
type UserSettingsProps = {
children: React.ReactNode;
};
const UserSettings = ({ children }: UserSettingsProps) => {
const router = useRouter();
const settings = useSettings();
const { user: currentUser } = useUser();
@@ -53,10 +57,7 @@ const UserSettings: React.FC = ({ children }) => {
regex: /\/settings\/password/,
hidden:
(!settings.currentSettings.localLogin &&
!hasPermission(
Permission.MANAGE_SETTINGS,
currentUser?.permissions ?? 0
)) ||
!hasPermission(Permission.ADMIN, currentUser?.permissions ?? 0)) ||
(currentUser?.id !== 1 &&
currentUser?.id !== user?.id &&
hasPermission(Permission.ADMIN, user?.permissions ?? 0)),

View File

@@ -1,30 +1,30 @@
import ImageFader from '@app/components/Common/ImageFader';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import ProgressCircle from '@app/components/Common/ProgressCircle';
import RequestCard from '@app/components/RequestCard';
import Slider from '@app/components/Slider';
import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard';
import ProfileHeader from '@app/components/UserProfile/ProfileHeader';
import { Permission, UserType, useUser } from '@app/hooks/useUser';
import Error from '@app/pages/_error';
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from 'react';
import { defineMessages, FormattedNumber, useIntl } from 'react-intl';
import useSWR from 'swr';
import {
import type { WatchlistResponse } from '@server/interfaces/api/discoverInterfaces';
import type {
QuotaResponse,
UserRequestsResponse,
UserWatchDataResponse,
} from '../../../server/interfaces/api/userInterfaces';
import { MovieDetails } from '../../../server/models/Movie';
import { TvDetails } from '../../../server/models/Tv';
import { Permission, UserType, useUser } from '../../hooks/useUser';
import Error from '../../pages/_error';
import ImageFader from '../Common/ImageFader';
import LoadingSpinner from '../Common/LoadingSpinner';
import PageTitle from '../Common/PageTitle';
import ProgressCircle from '../Common/ProgressCircle';
import RequestCard from '../RequestCard';
import Slider from '../Slider';
import TmdbTitleCard from '../TitleCard/TmdbTitleCard';
import ProfileHeader from './ProfileHeader';
} from '@server/interfaces/api/userInterfaces';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
const messages = defineMessages({
recentrequests: 'Recent Requests',
norequests: 'No requests.',
limit: '{remaining} of {limit}',
requestsperdays: '{limit} remaining',
unlimited: 'Unlimited',
@@ -33,11 +33,14 @@ const messages = defineMessages({
movierequests: 'Movie Requests',
seriesrequest: 'Series Requests',
recentlywatched: 'Recently Watched',
plexwatchlist: 'Plex Watchlist',
emptywatchlist:
'Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.',
});
type MediaTitle = MovieDetails | TvDetails;
const UserProfile: React.FC = () => {
const UserProfile = () => {
const intl = useIntl();
const router = useRouter();
const { user, error } = useUser({
@@ -68,12 +71,29 @@ const UserProfile: React.FC = () => {
? `/api/v1/user/${user.id}/quota`
: null
);
const { data: watchData } = useSWR<UserWatchDataResponse>(
user?.userType === UserType.PLEX &&
(user.id === currentUser?.id || currentHasPermission(Permission.ADMIN))
? `/api/v1/user/${user.id}/watch_data`
: null
);
const { data: watchData, error: watchDataError } =
useSWR<UserWatchDataResponse>(
user?.userType === UserType.PLEX &&
(user.id === currentUser?.id || currentHasPermission(Permission.ADMIN))
? `/api/v1/user/${user.id}/watch_data`
: null
);
const { data: watchlistItems, error: watchlistError } =
useSWR<WatchlistResponse>(
user?.userType === UserType.PLEX &&
(user.id === currentUser?.id ||
currentHasPermission(
[Permission.MANAGE_REQUESTS, Permission.WATCHLIST_VIEW],
{
type: 'or',
}
))
? `/api/v1/user/${user.id}/watchlist`
: null,
{
revalidateOnMount: true,
}
);
const updateAvailableTitles = useCallback(
(requestId: number, mediaTitle: MediaTitle) => {
@@ -129,7 +149,15 @@ const UserProfile: React.FC = () => {
{intl.formatMessage(messages.totalrequests)}
</dt>
<dd className="mt-1 text-3xl font-semibold text-white">
<FormattedNumber value={user.requestCount} />
<Link
href={
user.id === currentUser?.id
? '/profile/requests?filter=all'
: `/users/${user?.id}/requests?filter=all`
}
>
<a>{intl.formatNumber(user.requestCount)}</a>
</Link>
</dd>
</div>
<div
@@ -249,37 +277,97 @@ const UserProfile: React.FC = () => {
currentHasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
{ type: 'or' }
)) && (
<>
<div className="slider-header">
<Link href={`/users/${user?.id}/requests?filter=all`}>
<a className="slider-title">
<span>{intl.formatMessage(messages.recentrequests)}</span>
<ArrowCircleRightIcon />
</a>
</Link>
</div>
<Slider
sliderKey="requests"
isLoading={!requests && !requestError}
isEmpty={
!!requests && !requestError && requests.results.length === 0
}
items={(requests?.results ?? []).map((request) => (
<RequestCard
key={`request-slider-item-${request.id}`}
request={request}
onTitleData={updateAvailableTitles}
/>
))}
placeholder={<RequestCard.Placeholder />}
emptyMessage={intl.formatMessage(messages.norequests)}
/>
</>
)}
{(user.id === currentUser?.id ||
currentHasPermission(Permission.ADMIN)) &&
!!watchData?.recentlyWatched.length && (
)) &&
(!requests || !!requests.results.length) &&
!requestError && (
<>
<div className="slider-header">
<Link
href={
user.id === currentUser?.id
? '/profile/requests?filter=all'
: `/users/${user?.id}/requests?filter=all`
}
>
<a className="slider-title">
<span>{intl.formatMessage(messages.recentrequests)}</span>
<ArrowCircleRightIcon />
</a>
</Link>
</div>
<Slider
sliderKey="requests"
isLoading={!requests}
items={(requests?.results ?? []).map((request) => (
<RequestCard
key={`request-slider-item-${request.id}`}
request={request}
onTitleData={updateAvailableTitles}
/>
))}
placeholder={<RequestCard.Placeholder />}
/>
</>
)}
{user.userType === UserType.PLEX &&
(user.id === currentUser?.id ||
currentHasPermission(
[Permission.MANAGE_REQUESTS, Permission.WATCHLIST_VIEW],
{ type: 'or' }
)) &&
(!watchlistItems ||
!!watchlistItems.results.length ||
(user.id === currentUser?.id &&
(user.settings?.watchlistSyncMovies ||
user.settings?.watchlistSyncTv))) &&
!watchlistError && (
<>
<div className="slider-header">
<Link
href={
user.id === currentUser?.id
? '/profile/watchlist'
: `/users/${user?.id}/watchlist`
}
>
<a className="slider-title">
<span>{intl.formatMessage(messages.plexwatchlist)}</span>
<ArrowCircleRightIcon />
</a>
</Link>
</div>
<Slider
sliderKey="watchlist"
isLoading={!watchlistItems}
isEmpty={!!watchlistItems && watchlistItems.results.length === 0}
emptyMessage={intl.formatMessage(messages.emptywatchlist, {
PlexWatchlistSupportLink: (msg: React.ReactNode) => (
<a
href="https://support.plex.tv/articles/universal-watchlist/"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
items={watchlistItems?.results.map((item) => (
<TmdbTitleCard
id={item.tmdbId}
key={`watchlist-slider-item-${item.ratingKey}`}
tmdbId={item.tmdbId}
type={item.mediaType}
/>
))}
/>
</>
)}
{user.userType === UserType.PLEX &&
(user.id === currentUser?.id ||
currentHasPermission(Permission.ADMIN)) &&
(!watchData || !!watchData.recentlyWatched.length) &&
!watchDataError && (
<>
<div className="slider-header">
<div className="slider-title">
@@ -289,11 +377,12 @@ const UserProfile: React.FC = () => {
<Slider
sliderKey="media"
isLoading={!watchData}
isEmpty={!watchData?.recentlyWatched.length}
items={watchData.recentlyWatched.map((item) => (
items={watchData?.recentlyWatched.map((item) => (
<TmdbTitleCard
key={`media-slider-item-${item.id}`}
id={item.id}
tmdbId={item.tmdbId}
tvdbId={item.tvdbId}
type={item.mediaType}
/>
))}