diff --git a/src/components/Layout/LanguagePicker/index.tsx b/src/components/Layout/LanguagePicker/index.tsx
index 683fe5f43..cd589dde6 100644
--- a/src/components/Layout/LanguagePicker/index.tsx
+++ b/src/components/Layout/LanguagePicker/index.tsx
@@ -1,93 +1,22 @@
import { TranslateIcon } from '@heroicons/react/solid';
-import React, { useContext, useRef, useState } from 'react';
+import React, { useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import {
+ availableLanguages,
AvailableLocales,
- LanguageContext,
} from '../../../context/LanguageContext';
import useClickOutside from '../../../hooks/useClickOutside';
+import useLocale from '../../../hooks/useLocale';
import Transition from '../../Transition';
const messages = defineMessages({
changelanguage: 'Change Language',
});
-type AvailableLanguageObject = Record<
- string,
- { code: AvailableLocales; display: string }
->;
-
-const availableLanguages: AvailableLanguageObject = {
- ca: {
- code: 'ca',
- display: 'Català',
- },
- de: {
- code: 'de',
- display: 'Deutsch',
- },
- en: {
- code: 'en',
- display: 'English',
- },
- es: {
- code: 'es',
- display: 'Español',
- },
- fr: {
- code: 'fr',
- display: 'Français',
- },
- it: {
- code: 'it',
- display: 'Italiano',
- },
- hu: {
- code: 'hu',
- display: 'Magyar',
- },
- nl: {
- code: 'nl',
- display: 'Nederlands',
- },
- 'nb-NO': {
- code: 'nb-NO',
- display: 'Norsk Bokmål',
- },
- 'pt-BR': {
- code: 'pt-BR',
- display: 'Português (Brasil)',
- },
- 'pt-PT': {
- code: 'pt-PT',
- display: 'Português (Portugal)',
- },
- sv: {
- code: 'sv',
- display: 'Svenska',
- },
- ru: {
- code: 'ru',
- display: 'pусский',
- },
- sr: {
- code: 'sr',
- display: 'српски језик',
- },
- ja: {
- code: 'ja',
- display: '日本語',
- },
- 'zh-TW': {
- code: 'zh-TW',
- display: '中文(臺灣)',
- },
-};
-
const LanguagePicker: React.FC = () => {
const intl = useIntl();
const dropdownRef = useRef
(null);
- const { locale, setLocale } = useContext(LanguageContext);
+ const { locale, setLocale } = useLocale();
const [isDropdownOpen, setDropdownOpen] = useState(false);
useClickOutside(dropdownRef, () => setDropdownOpen(false));
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
index 7ea9ac64d..662868354 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -1,10 +1,9 @@
import { MenuAlt2Icon } from '@heroicons/react/outline';
-import { InformationCircleIcon } from '@heroicons/react/solid';
+import { ArrowLeftIcon, InformationCircleIcon } from '@heroicons/react/solid';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Permission, useUser } from '../../hooks/useUser';
-import LanguagePicker from './LanguagePicker';
import SearchInput from './SearchInput';
import Sidebar from './Sidebar';
import UserDropdown from './UserDropdown';
@@ -23,7 +22,7 @@ const Layout: React.FC = ({ children }) => {
useEffect(() => {
const updateScrolled = () => {
- if (window.pageYOffset > 60) {
+ if (window.pageYOffset > 20) {
setIsScrolled(true);
} else {
setIsScrolled(false);
@@ -55,16 +54,25 @@ const Layout: React.FC = ({ children }) => {
}}
>
-
+
+
-
diff --git a/src/components/MediaSlider/index.tsx b/src/components/MediaSlider/index.tsx
index 64aa79153..dcb7eea4f 100644
--- a/src/components/MediaSlider/index.tsx
+++ b/src/components/MediaSlider/index.tsx
@@ -1,6 +1,6 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
-import React, { useContext, useEffect } from 'react';
+import React, { useEffect } from 'react';
import { useSWRInfinite } from 'swr';
import { MediaStatus } from '../../../server/constants/media';
import type {
@@ -8,7 +8,6 @@ import type {
PersonResult,
TvResult,
} from '../../../server/models/Search';
-import { LanguageContext } from '../../context/LanguageContext';
import useSettings from '../../hooks/useSettings';
import PersonCard from '../PersonCard';
import Slider from '../Slider';
@@ -38,14 +37,13 @@ const MediaSlider: React.FC
= ({
hideWhenEmpty = false,
}) => {
const settings = useSettings();
- const { locale } = useContext(LanguageContext);
const { data, error, setSize, size } = useSWRInfinite(
(pageIndex: number, previousPageData: MixedResult | null) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
return null;
}
- return `${url}?page=${pageIndex + 1}&language=${locale}`;
+ return `${url}?page=${pageIndex + 1}`;
},
{
initialSize: 2,
diff --git a/src/components/MovieDetails/MovieCast/index.tsx b/src/components/MovieDetails/MovieCast/index.tsx
index 081a7a6d7..0cc9c2e03 100644
--- a/src/components/MovieDetails/MovieCast/index.tsx
+++ b/src/components/MovieDetails/MovieCast/index.tsx
@@ -1,15 +1,14 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
-import React, { useContext } from 'react';
+import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { MovieDetails } from '../../../../server/models/Movie';
-import { LanguageContext } from '../../../context/LanguageContext';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
-import PersonCard from '../../PersonCard';
import PageTitle from '../../Common/PageTitle';
+import PersonCard from '../../PersonCard';
const messages = defineMessages({
fullcast: 'Full Cast',
@@ -18,9 +17,8 @@ const messages = defineMessages({
const MovieCast: React.FC = () => {
const router = useRouter();
const intl = useIntl();
- const { locale } = useContext(LanguageContext);
const { data, error } = useSWR(
- `/api/v1/movie/${router.query.movieId}?language=${locale}`
+ `/api/v1/movie/${router.query.movieId}`
);
if (!data && !error) {
diff --git a/src/components/MovieDetails/MovieCrew/index.tsx b/src/components/MovieDetails/MovieCrew/index.tsx
index f19cbc205..14268e425 100644
--- a/src/components/MovieDetails/MovieCrew/index.tsx
+++ b/src/components/MovieDetails/MovieCrew/index.tsx
@@ -1,15 +1,14 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
-import React, { useContext } from 'react';
+import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { MovieDetails } from '../../../../server/models/Movie';
-import { LanguageContext } from '../../../context/LanguageContext';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
-import PersonCard from '../../PersonCard';
import PageTitle from '../../Common/PageTitle';
+import PersonCard from '../../PersonCard';
const messages = defineMessages({
fullcrew: 'Full Crew',
@@ -18,9 +17,8 @@ const messages = defineMessages({
const MovieCrew: React.FC = () => {
const router = useRouter();
const intl = useIntl();
- const { locale } = useContext(LanguageContext);
const { data, error } = useSWR(
- `/api/v1/movie/${router.query.movieId}?language=${locale}`
+ `/api/v1/movie/${router.query.movieId}`
);
if (!data && !error) {
diff --git a/src/components/MovieDetails/MovieRecommendations.tsx b/src/components/MovieDetails/MovieRecommendations.tsx
index b603e7b5b..fc9c2bf2c 100644
--- a/src/components/MovieDetails/MovieRecommendations.tsx
+++ b/src/components/MovieDetails/MovieRecommendations.tsx
@@ -1,16 +1,15 @@
-import React, { useContext } from 'react';
-import useSWR from 'swr';
-import type { MovieResult } from '../../../server/models/Search';
-import ListView from '../Common/ListView';
+import Link from 'next/link';
import { useRouter } from 'next/router';
-import Header from '../Common/Header';
-import type { MovieDetails } from '../../../server/models/Movie';
-import { LanguageContext } from '../../context/LanguageContext';
+import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
-import PageTitle from '../Common/PageTitle';
+import useSWR from 'swr';
+import type { MovieDetails } from '../../../server/models/Movie';
+import type { MovieResult } from '../../../server/models/Search';
import useDiscover from '../../hooks/useDiscover';
import Error from '../../pages/_error';
-import Link from 'next/link';
+import Header from '../Common/Header';
+import ListView from '../Common/ListView';
+import PageTitle from '../Common/PageTitle';
const messages = defineMessages({
recommendations: 'Recommendations',
@@ -19,9 +18,8 @@ const messages = defineMessages({
const MovieRecommendations: React.FC = () => {
const intl = useIntl();
const router = useRouter();
- const { locale } = useContext(LanguageContext);
const { data: movieData } = useSWR(
- `/api/v1/movie/${router.query.movieId}?language=${locale}`
+ `/api/v1/movie/${router.query.movieId}`
);
const {
isLoadingInitialData,
diff --git a/src/components/MovieDetails/MovieSimilar.tsx b/src/components/MovieDetails/MovieSimilar.tsx
index 93bacc366..8103f966e 100644
--- a/src/components/MovieDetails/MovieSimilar.tsx
+++ b/src/components/MovieDetails/MovieSimilar.tsx
@@ -1,16 +1,15 @@
-import React, { useContext } from 'react';
-import useSWR from 'swr';
-import type { MovieResult } from '../../../server/models/Search';
-import ListView from '../Common/ListView';
+import Link from 'next/link';
import { useRouter } from 'next/router';
-import Header from '../Common/Header';
-import { LanguageContext } from '../../context/LanguageContext';
-import type { MovieDetails } from '../../../server/models/Movie';
+import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
-import PageTitle from '../Common/PageTitle';
+import useSWR from 'swr';
+import type { MovieDetails } from '../../../server/models/Movie';
+import type { MovieResult } from '../../../server/models/Search';
import useDiscover from '../../hooks/useDiscover';
import Error from '../../pages/_error';
-import Link from 'next/link';
+import Header from '../Common/Header';
+import ListView from '../Common/ListView';
+import PageTitle from '../Common/PageTitle';
const messages = defineMessages({
similar: 'Similar Titles',
@@ -19,9 +18,8 @@ const messages = defineMessages({
const MovieSimilar: React.FC = () => {
const router = useRouter();
const intl = useIntl();
- const { locale } = useContext(LanguageContext);
const { data: movieData } = useSWR(
- `/api/v1/movie/${router.query.movieId}?language=${locale}`
+ `/api/v1/movie/${router.query.movieId}`
);
const {
isLoadingInitialData,
diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx
index 7db6b9465..8675898cb 100644
--- a/src/components/MovieDetails/index.tsx
+++ b/src/components/MovieDetails/index.tsx
@@ -12,7 +12,7 @@ import {
import axios from 'axios';
import Link from 'next/link';
import { useRouter } from 'next/router';
-import React, { useContext, useMemo, useState } from 'react';
+import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { RTRating } from '../../../server/api/rottentomatoes';
@@ -23,7 +23,7 @@ import RTAudRotten from '../../assets/rt_aud_rotten.svg';
import RTFresh from '../../assets/rt_fresh.svg';
import RTRotten from '../../assets/rt_rotten.svg';
import TmdbLogo from '../../assets/tmdb_logo.svg';
-import { LanguageContext } from '../../context/LanguageContext';
+import useLocale from '../../hooks/useLocale';
import useSettings from '../../hooks/useSettings';
import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
@@ -84,11 +84,11 @@ const MovieDetails: React.FC = ({ movie }) => {
const { user, hasPermission } = useUser();
const router = useRouter();
const intl = useIntl();
- const { locale } = useContext(LanguageContext);
+ const { locale } = useLocale();
const [showManager, setShowManager] = useState(false);
const { data, error, revalidate } = useSWR(
- `/api/v1/movie/${router.query.movieId}?language=${locale}`,
+ `/api/v1/movie/${router.query.movieId}`,
{
initialData: movie,
}
diff --git a/src/components/NotificationTypeSelector/index.tsx b/src/components/NotificationTypeSelector/index.tsx
index b549613f7..273500070 100644
--- a/src/components/NotificationTypeSelector/index.tsx
+++ b/src/components/NotificationTypeSelector/index.tsx
@@ -53,6 +53,10 @@ export enum Notification {
MEDIA_AUTO_APPROVED = 128,
}
+export const ALL_NOTIFICATIONS = Object.values(Notification)
+ .filter((v) => !isNaN(Number(v)))
+ .reduce((a, v) => a + Number(v), 0);
+
export interface NotificationItem {
id: string;
name: string;
diff --git a/src/components/PWAHeader/index.tsx b/src/components/PWAHeader/index.tsx
new file mode 100644
index 000000000..d8a9eba13
--- /dev/null
+++ b/src/components/PWAHeader/index.tsx
@@ -0,0 +1,183 @@
+import React from 'react';
+
+interface PWAHeaderProps {
+ applicationTitle?: string;
+}
+
+const PWAHeader: React.FC = ({ applicationTitle }) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default PWAHeader;
diff --git a/src/components/PersonDetails/index.tsx b/src/components/PersonDetails/index.tsx
index a0082c79c..3ea148c06 100644
--- a/src/components/PersonDetails/index.tsx
+++ b/src/components/PersonDetails/index.tsx
@@ -1,13 +1,12 @@
import { groupBy } from 'lodash';
import { useRouter } from 'next/router';
-import React, { useContext, useMemo, useState } from 'react';
+import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import TruncateMarkup from 'react-truncate-markup';
import useSWR from 'swr';
import type { PersonCombinedCreditsResponse } from '../../../server/interfaces/api/personInterfaces';
import type { PersonDetail } from '../../../server/models/Person';
import Ellipsis from '../../assets/ellipsis.svg';
-import { LanguageContext } from '../../context/LanguageContext';
import globalMessages from '../../i18n/globalMessages';
import Error from '../../pages/_error';
import CachedImage from '../Common/CachedImage';
@@ -27,10 +26,9 @@ const messages = defineMessages({
const PersonDetails: React.FC = () => {
const intl = useIntl();
- const { locale } = useContext(LanguageContext);
const router = useRouter();
const { data, error } = useSWR(
- `/api/v1/person/${router.query.personId}?language=${locale}`
+ `/api/v1/person/${router.query.personId}`
);
const [showBio, setShowBio] = useState(false);
@@ -38,7 +36,7 @@ const PersonDetails: React.FC = () => {
data: combinedCredits,
error: errorCombinedCredits,
} = useSWR(
- `/api/v1/person/${router.query.personId}/combined_credits?language=${locale}`
+ `/api/v1/person/${router.query.personId}/combined_credits`
);
const sortedCast = useMemo(() => {
diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx
index 7e71813e3..867795e12 100644
--- a/src/components/RequestCard/index.tsx
+++ b/src/components/RequestCard/index.tsx
@@ -1,7 +1,7 @@
import { CheckIcon, TrashIcon, XIcon } from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
-import React, { useContext, useEffect } from 'react';
+import React, { useEffect } from 'react';
import { useInView } from 'react-intersection-observer';
import { defineMessages, useIntl } from 'react-intl';
import useSWR, { mutate } from 'swr';
@@ -12,7 +12,6 @@ import {
import type { MediaRequest } from '../../../server/entity/MediaRequest';
import type { MovieDetails } from '../../../server/models/Movie';
import type { TvDetails } from '../../../server/models/Tv';
-import { LanguageContext } from '../../context/LanguageContext';
import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
import { withProperties } from '../../utils/typeHelpers';
@@ -92,13 +91,12 @@ const RequestCard: React.FC = ({ request, onTitleData }) => {
});
const intl = useIntl();
const { hasPermission } = useUser();
- const { locale } = useContext(LanguageContext);
const url =
request.type === 'movie'
? `/api/v1/movie/${request.media.tmdbId}`
: `/api/v1/tv/${request.media.tmdbId}`;
const { data: title, error } = useSWR(
- inView ? `${url}?language=${locale}` : null
+ inView ? `${url}` : null
);
const {
data: requestData,
diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx
index 01fb1ddc1..84de66cb3 100644
--- a/src/components/RequestList/RequestItem/index.tsx
+++ b/src/components/RequestList/RequestItem/index.tsx
@@ -7,7 +7,7 @@ import {
} from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
-import React, { useContext, useState } from 'react';
+import React, { useState } from 'react';
import { useInView } from 'react-intersection-observer';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@@ -19,7 +19,6 @@ import {
import type { MediaRequest } from '../../../../server/entity/MediaRequest';
import type { MovieDetails } from '../../../../server/models/Movie';
import type { TvDetails } from '../../../../server/models/Tv';
-import { LanguageContext } from '../../../context/LanguageContext';
import { Permission, useUser } from '../../../hooks/useUser';
import globalMessages from '../../../i18n/globalMessages';
import Badge from '../../Common/Badge';
@@ -99,13 +98,12 @@ const RequestItem: React.FC = ({
const intl = useIntl();
const { user, hasPermission } = useUser();
const [showEditModal, setShowEditModal] = useState(false);
- const { locale } = useContext(LanguageContext);
const url =
request.type === 'movie'
? `/api/v1/movie/${request.media.tmdbId}`
: `/api/v1/tv/${request.media.tmdbId}`;
const { data: title, error } = useSWR(
- inView ? `${url}?language=${locale}` : null
+ inView ? `${url}` : null
);
const { data: requestData, revalidate, mutate } = useSWR(
`/api/v1/request/${request.id}`,
diff --git a/src/components/ServiceWorkerSetup/index.tsx b/src/components/ServiceWorkerSetup/index.tsx
new file mode 100644
index 000000000..56a558a3d
--- /dev/null
+++ b/src/components/ServiceWorkerSetup/index.tsx
@@ -0,0 +1,49 @@
+/* eslint-disable no-console */
+import axios from 'axios';
+import React, { useEffect } from 'react';
+import useSettings from '../../hooks/useSettings';
+import { useUser } from '../../hooks/useUser';
+
+const ServiceWorkerSetup: React.FC = () => {
+ const { currentSettings } = useSettings();
+ const { user } = useUser();
+ useEffect(() => {
+ if ('serviceWorker' in navigator && user?.id) {
+ navigator.serviceWorker
+ .register('/sw.js')
+ .then(async (registration) => {
+ console.log(
+ '[SW] Registration successful, scope is:',
+ registration.scope
+ );
+
+ if (currentSettings.enablePushRegistration) {
+ const sub = await registration.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: currentSettings.vapidPublic,
+ });
+
+ const parsedSub = JSON.parse(JSON.stringify(sub));
+
+ if (parsedSub.keys.p256dh && parsedSub.keys.auth) {
+ await axios.post('/api/v1/user/registerPushSubscription', {
+ endpoint: parsedSub.endpoint,
+ p256dh: parsedSub.keys.p256dh,
+ auth: parsedSub.keys.auth,
+ });
+ }
+ }
+ })
+ .catch(function (error) {
+ console.log('[SW] Service worker registration failed, error:', error);
+ });
+ }
+ }, [
+ user,
+ currentSettings.vapidPublic,
+ currentSettings.enablePushRegistration,
+ ]);
+ return null;
+};
+
+export default ServiceWorkerSetup;
diff --git a/src/components/Settings/Notifications/NotificationsWebPush/index.tsx b/src/components/Settings/Notifications/NotificationsWebPush/index.tsx
new file mode 100644
index 000000000..c1db453e0
--- /dev/null
+++ b/src/components/Settings/Notifications/NotificationsWebPush/index.tsx
@@ -0,0 +1,122 @@
+import axios from 'axios';
+import { Field, Form, Formik } from 'formik';
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import { useToasts } from 'react-toast-notifications';
+import useSWR from 'swr';
+import globalMessages from '../../../../i18n/globalMessages';
+import Button from '../../../Common/Button';
+import LoadingSpinner from '../../../Common/LoadingSpinner';
+import NotificationTypeSelector from '../../../NotificationTypeSelector';
+
+const messages = defineMessages({
+ agentenabled: 'Enable Agent',
+ webpushsettingssaved: 'Web push notification settings saved successfully!',
+ webpushsettingsfailed: 'Web push notification settings failed to save.',
+ testsent: 'Web push test notification sent!',
+});
+
+const NotificationsWebPush: React.FC = () => {
+ const intl = useIntl();
+ const { addToast } = useToasts();
+ const { data, error, revalidate } = useSWR(
+ '/api/v1/settings/notifications/webpush'
+ );
+
+ if (!data && !error) {
+ return ;
+ }
+
+ return (
+ <>
+ {
+ try {
+ await axios.post('/api/v1/settings/notifications/webpush', {
+ enabled: values.enabled,
+ types: values.types,
+ options: {},
+ });
+ addToast(intl.formatMessage(messages.webpushsettingssaved), {
+ appearance: 'success',
+ autoDismiss: true,
+ });
+ } catch (e) {
+ addToast(intl.formatMessage(messages.webpushsettingsfailed), {
+ appearance: 'error',
+ autoDismiss: true,
+ });
+ } finally {
+ revalidate();
+ }
+ }}
+ >
+ {({ isSubmitting, values, isValid, setFieldValue }) => {
+ const testSettings = async () => {
+ await axios.post('/api/v1/settings/notifications/webpush/test', {
+ enabled: true,
+ types: values.types,
+ options: {},
+ });
+
+ addToast(intl.formatMessage(messages.testsent), {
+ appearance: 'info',
+ autoDismiss: true,
+ });
+ };
+
+ return (
+
+ );
+ }}
+
+ >
+ );
+};
+
+export default NotificationsWebPush;
diff --git a/src/components/Settings/SettingsNotifications.tsx b/src/components/Settings/SettingsNotifications.tsx
index 3c73c001b..88cfb274f 100644
--- a/src/components/Settings/SettingsNotifications.tsx
+++ b/src/components/Settings/SettingsNotifications.tsx
@@ -1,5 +1,5 @@
import { AtSymbolIcon } from '@heroicons/react/outline';
-import { LightningBoltIcon } from '@heroicons/react/solid';
+import { CloudIcon, LightningBoltIcon } from '@heroicons/react/solid';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import DiscordLogo from '../../assets/extlogos/discord.svg';
@@ -18,6 +18,7 @@ const messages = defineMessages({
'Configure and enable notification agents.',
email: 'Email',
webhook: 'Webhook',
+ webpush: 'Web Push',
});
const SettingsNotifications: React.FC = ({ children }) => {
@@ -90,6 +91,17 @@ const SettingsNotifications: React.FC = ({ children }) => {
route: '/settings/notifications/telegram',
regex: /^\/settings\/notifications\/telegram/,
},
+ {
+ text: intl.formatMessage(messages.webpush),
+ content: (
+
+
+ {intl.formatMessage(messages.webpush)}
+
+ ),
+ route: '/settings/notifications/webpush',
+ regex: /^\/settings\/notifications\/webpush/,
+ },
{
text: intl.formatMessage(messages.webhook),
content: (
diff --git a/src/components/TitleCard/TmdbTitleCard.tsx b/src/components/TitleCard/TmdbTitleCard.tsx
index 40325a30b..a783037a1 100644
--- a/src/components/TitleCard/TmdbTitleCard.tsx
+++ b/src/components/TitleCard/TmdbTitleCard.tsx
@@ -1,10 +1,9 @@
-import React, { useContext } from 'react';
+import React from 'react';
import { useInView } from 'react-intersection-observer';
import useSWR from 'swr';
+import TitleCard from '.';
import type { MovieDetails } from '../../../server/models/Movie';
import type { TvDetails } from '../../../server/models/Tv';
-import TitleCard from '.';
-import { LanguageContext } from '../../context/LanguageContext';
interface TmdbTitleCardProps {
tmdbId: number;
@@ -19,11 +18,10 @@ const TmdbTitleCard: React.FC = ({ tmdbId, type }) => {
const { ref, inView } = useInView({
triggerOnce: true,
});
- const { locale } = useContext(LanguageContext);
const url =
type === 'movie' ? `/api/v1/movie/${tmdbId}` : `/api/v1/tv/${tmdbId}`;
const { data: title, error } = useSWR(
- inView ? `${url}?language=${locale}` : null
+ inView ? `${url}` : null
);
if (!title && !error) {
diff --git a/src/components/Toast/index.tsx b/src/components/Toast/index.tsx
index aaad91a33..92ae5a76f 100644
--- a/src/components/Toast/index.tsx
+++ b/src/components/Toast/index.tsx
@@ -7,41 +7,57 @@ import {
import { XIcon } from '@heroicons/react/solid';
import React from 'react';
import type { ToastProps } from 'react-toast-notifications';
+import Transition from '../Transition';
-const Toast: React.FC = ({ appearance, children, onDismiss }) => {
+const Toast: React.FC = ({
+ appearance,
+ children,
+ onDismiss,
+ transitionState,
+}) => {
return (
-
-
-
-
-
- {appearance === 'success' && (
-
- )}
- {appearance === 'error' && (
-
- )}
- {appearance === 'info' && (
-
- )}
- {appearance === 'warning' && (
-
- )}
-
-
{children}
-
-
+
+
+
+
+
+
+ {appearance === 'success' && (
+
+ )}
+ {appearance === 'error' && (
+
+ )}
+ {appearance === 'info' && (
+
+ )}
+ {appearance === 'warning' && (
+
+ )}
+
+
{children}
+
+
+
-
+
);
};
diff --git a/src/components/ToastContainer/index.tsx b/src/components/ToastContainer/index.tsx
new file mode 100644
index 000000000..ea481737f
--- /dev/null
+++ b/src/components/ToastContainer/index.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { ToastContainerProps } from 'react-toast-notifications';
+
+const ToastContainer: React.FC
= ({
+ hasToasts,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export default ToastContainer;
diff --git a/src/components/TvDetails/TvCast/index.tsx b/src/components/TvDetails/TvCast/index.tsx
index 78cfccc19..9631ad491 100644
--- a/src/components/TvDetails/TvCast/index.tsx
+++ b/src/components/TvDetails/TvCast/index.tsx
@@ -1,15 +1,14 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
-import React, { useContext } from 'react';
+import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { TvDetails } from '../../../../server/models/Tv';
-import { LanguageContext } from '../../../context/LanguageContext';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
-import PersonCard from '../../PersonCard';
import PageTitle from '../../Common/PageTitle';
+import PersonCard from '../../PersonCard';
const messages = defineMessages({
fullseriescast: 'Full Series Cast',
@@ -18,10 +17,7 @@ const messages = defineMessages({
const TvCast: React.FC = () => {
const router = useRouter();
const intl = useIntl();
- const { locale } = useContext(LanguageContext);
- const { data, error } = useSWR(
- `/api/v1/tv/${router.query.tvId}?language=${locale}`
- );
+ const { data, error } = useSWR(`/api/v1/tv/${router.query.tvId}`);
if (!data && !error) {
return ;
diff --git a/src/components/TvDetails/TvCrew/index.tsx b/src/components/TvDetails/TvCrew/index.tsx
index 64c1af834..5ed0297d2 100644
--- a/src/components/TvDetails/TvCrew/index.tsx
+++ b/src/components/TvDetails/TvCrew/index.tsx
@@ -1,15 +1,14 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
-import React, { useContext } from 'react';
+import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { TvDetails } from '../../../../server/models/Tv';
-import { LanguageContext } from '../../../context/LanguageContext';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
-import PersonCard from '../../PersonCard';
import PageTitle from '../../Common/PageTitle';
+import PersonCard from '../../PersonCard';
const messages = defineMessages({
fullseriescrew: 'Full Series Crew',
@@ -18,10 +17,7 @@ const messages = defineMessages({
const TvCrew: React.FC = () => {
const router = useRouter();
const intl = useIntl();
- const { locale } = useContext(LanguageContext);
- const { data, error } = useSWR(
- `/api/v1/tv/${router.query.tvId}?language=${locale}`
- );
+ const { data, error } = useSWR(`/api/v1/tv/${router.query.tvId}`);
if (!data && !error) {
return ;
diff --git a/src/components/TvDetails/TvRecommendations.tsx b/src/components/TvDetails/TvRecommendations.tsx
index c5aa7b04a..94e6f761b 100644
--- a/src/components/TvDetails/TvRecommendations.tsx
+++ b/src/components/TvDetails/TvRecommendations.tsx
@@ -1,16 +1,15 @@
-import React, { useContext } from 'react';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { TvResult } from '../../../server/models/Search';
-import ListView from '../Common/ListView';
-import { useRouter } from 'next/router';
-import { LanguageContext } from '../../context/LanguageContext';
-import Header from '../Common/Header';
-import { defineMessages, useIntl } from 'react-intl';
import { TvDetails } from '../../../server/models/Tv';
-import PageTitle from '../Common/PageTitle';
-import Error from '../../pages/_error';
import useDiscover from '../../hooks/useDiscover';
-import Link from 'next/link';
+import Error from '../../pages/_error';
+import Header from '../Common/Header';
+import ListView from '../Common/ListView';
+import PageTitle from '../Common/PageTitle';
const messages = defineMessages({
recommendations: 'Recommendations',
@@ -19,10 +18,7 @@ const messages = defineMessages({
const TvRecommendations: React.FC = () => {
const router = useRouter();
const intl = useIntl();
- const { locale } = useContext(LanguageContext);
- const { data: tvData } = useSWR(
- `/api/v1/tv/${router.query.tvId}?language=${locale}`
- );
+ const { data: tvData } = useSWR(`/api/v1/tv/${router.query.tvId}`);
const {
isLoadingInitialData,
isEmpty,
diff --git a/src/components/TvDetails/TvSimilar.tsx b/src/components/TvDetails/TvSimilar.tsx
index c09cca28a..a82147470 100644
--- a/src/components/TvDetails/TvSimilar.tsx
+++ b/src/components/TvDetails/TvSimilar.tsx
@@ -1,16 +1,15 @@
-import React, { useContext } from 'react';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { TvResult } from '../../../server/models/Search';
-import ListView from '../Common/ListView';
-import { useRouter } from 'next/router';
-import { LanguageContext } from '../../context/LanguageContext';
-import { useIntl, defineMessages } from 'react-intl';
import type { TvDetails } from '../../../server/models/Tv';
-import Header from '../Common/Header';
-import PageTitle from '../Common/PageTitle';
import useDiscover from '../../hooks/useDiscover';
import Error from '../../pages/_error';
-import Link from 'next/link';
+import Header from '../Common/Header';
+import ListView from '../Common/ListView';
+import PageTitle from '../Common/PageTitle';
const messages = defineMessages({
similar: 'Similar Series',
@@ -19,10 +18,7 @@ const messages = defineMessages({
const TvSimilar: React.FC = () => {
const router = useRouter();
const intl = useIntl();
- const { locale } = useContext(LanguageContext);
- const { data: tvData } = useSWR(
- `/api/v1/tv/${router.query.tvId}?language=${locale}`
- );
+ const { data: tvData } = useSWR(`/api/v1/tv/${router.query.tvId}`);
const {
isLoadingInitialData,
isEmpty,
diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx
index 69c0e9c39..8406f13ea 100644
--- a/src/components/TvDetails/index.tsx
+++ b/src/components/TvDetails/index.tsx
@@ -12,7 +12,7 @@ import {
import axios from 'axios';
import Link from 'next/link';
import { useRouter } from 'next/router';
-import React, { useContext, useMemo, useState } from 'react';
+import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { RTRating } from '../../../server/api/rottentomatoes';
@@ -25,7 +25,7 @@ import RTAudRotten from '../../assets/rt_aud_rotten.svg';
import RTFresh from '../../assets/rt_fresh.svg';
import RTRotten from '../../assets/rt_rotten.svg';
import TmdbLogo from '../../assets/tmdb_logo.svg';
-import { LanguageContext } from '../../context/LanguageContext';
+import useLocale from '../../hooks/useLocale';
import useSettings from '../../hooks/useSettings';
import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
@@ -91,12 +91,12 @@ const TvDetails: React.FC = ({ tv }) => {
const { user, hasPermission } = useUser();
const router = useRouter();
const intl = useIntl();
- const { locale } = useContext(LanguageContext);
+ const { locale } = useLocale();
const [showRequestModal, setShowRequestModal] = useState(false);
const [showManager, setShowManager] = useState(false);
const { data, error, revalidate } = useSWR(
- `/api/v1/tv/${router.query.tvId}?language=${locale}`,
+ `/api/v1/tv/${router.query.tvId}`,
{
initialData: tv,
}
diff --git a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx
index d9c455e20..280592977 100644
--- a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx
+++ b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx
@@ -7,6 +7,8 @@ import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import { UserSettingsGeneralResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { Language } from '../../../../../server/lib/settings';
+import { availableLanguages } 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';
@@ -39,11 +41,13 @@ const messages = defineMessages({
movierequestlimit: 'Movie Request Limit',
seriesrequestlimit: 'Series Request Limit',
enableOverride: 'Enable Override',
+ applanguage: 'Display Language',
});
const UserGeneralSettings: React.FC = () => {
const intl = useIntl();
const { addToast } = useToasts();
+ const { locale, setLocale } = useLocale();
const [movieQuotaEnabled, setMovieQuotaEnabled] = useState(false);
const [tvQuotaEnabled, setTvQuotaEnabled] = useState(false);
const router = useRouter();
@@ -115,6 +119,7 @@ const UserGeneralSettings: React.FC = () => {
{
movieQuotaDays: movieQuotaEnabled ? values.movieQuotaDays : null,
tvQuotaLimit: tvQuotaEnabled ? values.tvQuotaLimit : null,
tvQuotaDays: tvQuotaEnabled ? values.tvQuotaDays : null,
+ locale: values.locale,
});
+ if (setLocale) {
+ setLocale(values.locale);
+ }
+
addToast(intl.formatMessage(messages.toastSettingsSuccess), {
autoDismiss: true,
appearance: 'success',
@@ -206,6 +216,24 @@ const UserGeneralSettings: React.FC = () => {
)}
+
+
+
+
+
+ {(Object.keys(
+ availableLanguages
+ ) as (keyof typeof availableLanguages)[]).map((key) => (
+
+ ))}
+
+
+
+
diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx
index b949fb95a..b8123c909 100644
--- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx
+++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx
@@ -1,21 +1,18 @@
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
-import React, { useEffect, useState } from 'react';
+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 {
- hasNotificationAgentEnabled,
- NotificationAgentType,
-} from '../../../../../server/lib/notifications/agenttypes';
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 { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector';
import { OpenPgpLink } from '../../../Settings/Notifications/NotificationsEmail';
const messages = defineMessages({
@@ -32,18 +29,11 @@ const UserEmailSettings: React.FC = () => {
const intl = useIntl();
const { addToast } = useToasts();
const router = useRouter();
- const [notificationAgents, setNotificationAgents] = useState(0);
const { user } = useUser({ id: Number(router.query.userId) });
const { data, error, revalidate } = useSWR(
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
);
- useEffect(() => {
- setNotificationAgents(
- data?.notificationAgents ?? NotificationAgentType.EMAIL
- );
- }, [data]);
-
const UserNotificationsEmailSchema = Yup.object().shape({
pgpKey: Yup.string()
.nullable()
@@ -60,10 +50,7 @@ const UserEmailSettings: React.FC = () => {
return (
{
onSubmit={async (values) => {
try {
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
- notificationAgents,
pgpKey: values.pgpKey,
discordId: data?.discordId,
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
+ notificationTypes: {
+ email: values.enableEmail ? ALL_NOTIFICATIONS : 0,
+ },
});
addToast(intl.formatMessage(messages.emailsettingssaved), {
appearance: 'success',
@@ -91,7 +80,7 @@ const UserEmailSettings: React.FC = () => {
}
}}
>
- {({ errors, touched, isSubmitting, isValid, values, setFieldValue }) => {
+ {({ errors, touched, isSubmitting, isValid }) => {
return (