mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
feat(premium): let self-hosted instances sell premium and gifts (#3025)
This commit is contained in:
1 parent
192cec689a
commit
3736d94d73
159 files changed
+15802
-9067
No files matched your search
@@ -7,10 +7,10 @@ import {
|
||||
PREMIUM_PRODUCT_NAME,
|
||||
PRODUCT_NAME,
|
||||
} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import {GET_PREMIUM_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
|
||||
import * as PremiumModalCommands from '@app/features/premium/commands/PremiumModalCommands';
|
||||
import {shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
|
||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
@@ -143,7 +143,7 @@ export const VideoTab: React.FC<VideoTabProps> = observer(
|
||||
screenShareContentHint,
|
||||
} = voiceSettings;
|
||||
const hasHigherQuality = hasHigherVideoQuality();
|
||||
const isSelfHosted = RuntimeConfig.isSelfHosted();
|
||||
const showPremium = shouldShowPremiumFeatures();
|
||||
const {
|
||||
devices,
|
||||
deviceState,
|
||||
@@ -160,7 +160,7 @@ export const VideoTab: React.FC<VideoTabProps> = observer(
|
||||
const cameraResolutionOptions: ReadonlyArray<ComboboxOption<CameraResolution>> = [
|
||||
{value: 'low', label: LOW_480P_LABEL},
|
||||
{value: 'medium', label: MEDIUM_720P_LABEL},
|
||||
...(hasHigherQuality || !isSelfHosted
|
||||
...(hasHigherQuality || showPremium
|
||||
? [
|
||||
{
|
||||
value: 'high' as const,
|
||||
@@ -181,7 +181,7 @@ export const VideoTab: React.FC<VideoTabProps> = observer(
|
||||
const screenShareState = resolveUserVideoTabScreenShareState({
|
||||
quality: screenShareQuality,
|
||||
hintSetting: screenShareContentHint,
|
||||
selfHosted: isSelfHosted,
|
||||
selfHosted: !showPremium,
|
||||
});
|
||||
const screenshareResolutionOptions: ReadonlyArray<ComboboxOption<OfferedScreenShareResolution>> =
|
||||
screenShareState.resolutionOptions.map((option) => ({
|
||||
@@ -308,7 +308,7 @@ export const VideoTab: React.FC<VideoTabProps> = observer(
|
||||
dataFlx="user.video-tab.select.screenshare-resolution-change"
|
||||
data-flx="user.user-video-tab.video-tab.compact-combobox-row.screenshare-resolution-change"
|
||||
/>
|
||||
{!hasHigherQuality && !isSelfHosted && (
|
||||
{!hasHigherQuality && showPremium && (
|
||||
<div className={styles.premiumCard} data-flx="user.video-tab.premium-card">
|
||||
<div className={styles.premiumHeader} data-flx="user.video-tab.premium-header">
|
||||
<CrownIcon
|
||||
@@ -347,7 +347,7 @@ export const VideoTab: React.FC<VideoTabProps> = observer(
|
||||
dataFlx="user.video-tab.select.frame-rate-change"
|
||||
data-flx="user.user-video-tab.video-tab.compact-combobox-row.video-frame-rate-change"
|
||||
/>
|
||||
{!hasHigherQuality && !isSelfHosted && (
|
||||
{!hasHigherQuality && showPremium && (
|
||||
<div className={styles.frameRateNote} data-flx="user.video-tab.frame-rate-note">
|
||||
<CrownIcon
|
||||
weight="fill"
|
||||
@@ -358,7 +358,7 @@ export const VideoTab: React.FC<VideoTabProps> = observer(
|
||||
{i18n._(HIGH_FRAME_RATE_REQUIRES_PREMIUM_DESCRIPTOR, {premiumProductName: PREMIUM_PRODUCT_NAME})}
|
||||
</div>
|
||||
)}
|
||||
{!hasHigherQuality && isSelfHosted && (
|
||||
{!hasHigherQuality && !showPremium && (
|
||||
<div className={styles.frameRateNote} data-flx="user.video-tab.instance-limit-note">
|
||||
{i18n._(SELF_HOSTED_VIDEO_QUALITY_LIMIT_DESCRIPTOR)}
|
||||
</div>
|
||||
|
||||
@@ -76,3 +76,11 @@
|
||||
:global(:where(html.window-focused, html.unfocused-fully-interactive):not(.window-focus-activation-guard)) .link:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.linkButton {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import {Routes} from '@app/app/Routes';
|
||||
import {PREMIUM_PRODUCT_FULL_NAME, PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import {cdnUrl} from '@app/features/messaging/utils/MessagingUrlUtils';
|
||||
import * as PremiumModalCommands from '@app/features/premium/commands/PremiumModalCommands';
|
||||
import {shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
|
||||
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
||||
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
|
||||
import {handleExternalLinkClick} from '@app/features/ui/utils/NativeUtils';
|
||||
@@ -63,6 +65,7 @@ interface BaseBadge {
|
||||
key: string;
|
||||
tooltip: string;
|
||||
url?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface IconBadge extends BaseBadge {
|
||||
@@ -88,6 +91,8 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
|
||||
({user, profile, isModal = false, isMobile = false}) => {
|
||||
const {i18n} = useLingui();
|
||||
const selfHosted = RuntimeConfig.isSelfHosted();
|
||||
const showPremium = shouldShowPremiumFeatures();
|
||||
const premiumInfoUrl = RuntimeConfig.premiumInfoUrl;
|
||||
const badges = useMemo(() => {
|
||||
const result: Array<Badge> = [];
|
||||
if (user.flags & PublicUserFlags.STAFF) {
|
||||
@@ -117,10 +122,11 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
|
||||
url: Routes.bugs(),
|
||||
});
|
||||
}
|
||||
if (!selfHosted && profile?.premiumType && profile.premiumType !== UserPremiumTypes.NONE) {
|
||||
if (showPremium && profile?.premiumType && profile.premiumType !== UserPremiumTypes.NONE) {
|
||||
let tooltipText = PREMIUM_PRODUCT_FULL_NAME;
|
||||
let badgeUrl = Routes.plutonium();
|
||||
if (profile.premiumType === UserPremiumTypes.LIFETIME) {
|
||||
let badgeUrl: string | undefined = premiumInfoUrl ?? (selfHosted ? undefined : Routes.plutonium());
|
||||
const badgeOnClick = badgeUrl ? undefined : () => PremiumModalCommands.open();
|
||||
if (!selfHosted && profile.premiumType === UserPremiumTypes.LIFETIME) {
|
||||
if (profile.premiumSince) {
|
||||
const premiumSinceFormatted = DateUtils.getFormattedShortDate(profile.premiumSince);
|
||||
tooltipText = i18n._(VISIONARY_SINCE_DESCRIPTOR, {productName: PRODUCT_NAME, premiumSinceFormatted});
|
||||
@@ -141,8 +147,13 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
|
||||
iconUrl: badgeAssetUrl('plutonium.svg'),
|
||||
tooltip: tooltipText,
|
||||
url: badgeUrl,
|
||||
onClick: badgeOnClick,
|
||||
});
|
||||
if (profile.premiumType === UserPremiumTypes.LIFETIME && profile.premiumLifetimeSequence != null) {
|
||||
if (
|
||||
!selfHosted &&
|
||||
profile.premiumType === UserPremiumTypes.LIFETIME &&
|
||||
profile.premiumLifetimeSequence != null
|
||||
) {
|
||||
const visionaryIdLabel = `#${profile.premiumLifetimeSequence}`;
|
||||
result.push({
|
||||
type: 'text',
|
||||
@@ -156,6 +167,8 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
|
||||
return result;
|
||||
}, [
|
||||
selfHosted,
|
||||
showPremium,
|
||||
premiumInfoUrl,
|
||||
user.flags,
|
||||
profile?.premiumType,
|
||||
profile?.premiumSince,
|
||||
@@ -170,7 +183,23 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
|
||||
: styles.containerPopout;
|
||||
const badgeClassName = isModal && isMobile ? styles.badgeMobile : styles.badgeDesktop;
|
||||
const isDesktopInteractions = !isMobile;
|
||||
const renderInteractiveWrapper = (url: string | undefined, children: React.ReactNode) => {
|
||||
const renderInteractiveWrapper = (
|
||||
url: string | undefined,
|
||||
onClick: (() => void) | undefined,
|
||||
children: React.ReactNode,
|
||||
) => {
|
||||
if (onClick && isDesktopInteractions) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(styles.link, styles.linkButton)}
|
||||
onClick={onClick}
|
||||
data-flx="user.user-profile-badges.render-interactive-wrapper.button"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
if (url && isDesktopInteractions) {
|
||||
return (
|
||||
<a
|
||||
@@ -215,7 +244,7 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
|
||||
return (
|
||||
<Tooltip key={badge.key} text={badge.tooltip} maxWidth="xl" data-flx="user.user-profile-badges.tooltip">
|
||||
<FocusRing offset={-2} data-flx="user.user-profile-badges.focus-ring">
|
||||
{renderInteractiveWrapper(badge.url, badgeContent)}
|
||||
{renderInteractiveWrapper(badge.url, badge.onClick, badgeContent)}
|
||||
</FocusRing>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {PREMIUM_PRODUCT_FULL_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import {shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
|
||||
import {guessPlatform, isDesktop} from '@app/features/ui/utils/NativeUtils';
|
||||
import {
|
||||
ADVANCED_SETTINGS_TAG_LABELS,
|
||||
@@ -394,7 +394,7 @@ export const getSettingsTabs = (i18n: I18n): Array<SettingsTab> => {
|
||||
...tab,
|
||||
label: getUserSettingsTabLabel(i18n, tab.type),
|
||||
}));
|
||||
const isSelfHosted = RuntimeConfig.isSelfHosted();
|
||||
const showPremium = shouldShowPremiumFeatures();
|
||||
const showClaimedAccountUi = shouldShowClaimedAccountUi();
|
||||
return allTabs.filter((tab) => {
|
||||
if (!showClaimedAccountUi && (tab.type === 'my_profile' || tab.type === 'linked_accounts')) {
|
||||
@@ -403,7 +403,7 @@ export const getSettingsTabs = (i18n: I18n): Array<SettingsTab> => {
|
||||
if (ACCOUNT_NESTED_TAB_TYPES.some((tabType) => tabType === tab.type)) {
|
||||
return false;
|
||||
}
|
||||
if (isSelfHosted && (tab.type === 'plutonium' || tab.type === 'gift_inventory')) {
|
||||
if (!showPremium && (tab.type === 'plutonium' || tab.type === 'gift_inventory')) {
|
||||
return false;
|
||||
}
|
||||
if (tab.type === 'desktop_settings' && !isDesktop()) {
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import {shouldShowClaimedAccountSettings} from '@app/features/user/components/se
|
||||
import type {SearchableSettingDescriptor} from '@app/features/user/components/settings_utils/search_index/SearchIndexTypes';
|
||||
import {
|
||||
BACKGROUND_DESCRIPTOR,
|
||||
PLUTONIUM_DESCRIPTOR,
|
||||
PREMIUM_PRODUCT_NAME_KEYWORD,
|
||||
} from '@app/features/user/components/settings_utils/search_index/SharedDescriptors';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
@@ -311,7 +311,7 @@ export const myProfileIndex: Array<SearchableSettingDescriptor> = [
|
||||
id: 'profile-badge',
|
||||
tabType: 'my_profile',
|
||||
label: BADGE_DESCRIPTOR,
|
||||
keywords: [BADGE_DESCRIPTOR, PREMIUM_BADGE_DESCRIPTOR, PLUTONIUM_DESCRIPTOR],
|
||||
keywords: [BADGE_DESCRIPTOR, PREMIUM_BADGE_DESCRIPTOR, PREMIUM_PRODUCT_NAME_KEYWORD],
|
||||
description: CONFIGURE_YOUR_PROFILE_BADGE_DESCRIPTOR,
|
||||
isVisible: shouldShowPremiumBadgeSettings,
|
||||
},
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import type {SearchableSettingDescriptor} from '@app/features/user/components/se
|
||||
import {
|
||||
CODES_DESCRIPTOR,
|
||||
GIFTS_DESCRIPTOR,
|
||||
PLUTONIUM_DESCRIPTOR,
|
||||
PREMIUM_PRODUCT_NAME_KEYWORD,
|
||||
} from '@app/features/user/components/settings_utils/search_index/SharedDescriptors';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
@@ -157,7 +157,7 @@ export const plutoniumIndex: Array<SearchableSettingDescriptor> = [
|
||||
tabType: 'plutonium',
|
||||
label: SUBSCRIPTION_DESCRIPTOR,
|
||||
keywords: [
|
||||
PLUTONIUM_DESCRIPTOR,
|
||||
PREMIUM_PRODUCT_NAME_KEYWORD,
|
||||
PREMIUM_DESCRIPTOR,
|
||||
SUBSCRIPTION_DESCRIPTOR,
|
||||
UPGRADE_DESCRIPTOR,
|
||||
|
||||
+2
-4
@@ -1,15 +1,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {PREMIUM_PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
export const BACKGROUND_DESCRIPTOR = msg({
|
||||
message: 'Background',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
export const PLUTONIUM_DESCRIPTOR = msg({
|
||||
message: 'Plutonium',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
export const PREMIUM_PRODUCT_NAME_KEYWORD = PREMIUM_PRODUCT_NAME;
|
||||
export const LOGIN_DESCRIPTOR = msg({
|
||||
message: 'Login',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ import {
|
||||
MENTIONS_DESCRIPTOR,
|
||||
NARRATION_DESCRIPTOR,
|
||||
PING_DESCRIPTOR,
|
||||
PLUTONIUM_DESCRIPTOR,
|
||||
POPUP_DESCRIPTOR,
|
||||
PREMIUM_PRODUCT_NAME_KEYWORD,
|
||||
READ_ALOUD_DESCRIPTOR,
|
||||
READ_MESSAGES_DESCRIPTOR,
|
||||
SPEECH_DESCRIPTOR,
|
||||
@@ -362,7 +362,7 @@ export const notificationsSections = [
|
||||
CUSTOM_RINGTONE_DESCRIPTOR,
|
||||
MP3_DESCRIPTOR,
|
||||
WAV_DESCRIPTOR,
|
||||
PLUTONIUM_DESCRIPTOR,
|
||||
PREMIUM_PRODUCT_NAME_KEYWORD,
|
||||
VOLUME_DESCRIPTOR,
|
||||
MASTER_VOLUME_DESCRIPTOR,
|
||||
SOUND_VOLUME_DESCRIPTOR,
|
||||
|
||||
+2
-4
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {PREMIUM_PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
export const COLORS_DESCRIPTOR = msg({
|
||||
@@ -175,10 +176,7 @@ export const VOLUME_DESCRIPTOR = msg({
|
||||
message: 'Volume',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
export const PLUTONIUM_DESCRIPTOR = msg({
|
||||
message: 'Plutonium',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
export const PREMIUM_PRODUCT_NAME_KEYWORD = PREMIUM_PRODUCT_NAME;
|
||||
export const DM_DESCRIPTOR = msg({
|
||||
message: 'DM',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
import type {SectionDefinition} from '@app/features/user/components/settings_utils/section_registry/SectionRegistryTypes';
|
||||
import {
|
||||
AUDIO_2_DESCRIPTOR,
|
||||
PLUTONIUM_DESCRIPTOR,
|
||||
PREMIUM_PRODUCT_NAME_KEYWORD,
|
||||
VOICE_DESCRIPTOR,
|
||||
VOLUME_DESCRIPTOR,
|
||||
} from '@app/features/user/components/settings_utils/section_registry/SharedDescriptors';
|
||||
@@ -346,7 +346,7 @@ export const voiceVideoSections = [
|
||||
MESSAGE_1080P_DESCRIPTOR,
|
||||
MESSAGE_1440P_DESCRIPTOR,
|
||||
SOURCE_QUALITY_DESCRIPTOR,
|
||||
PLUTONIUM_DESCRIPTOR,
|
||||
PREMIUM_PRODUCT_NAME_KEYWORD,
|
||||
],
|
||||
isAdvanced: false,
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user