mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
feat(profile): ship profile timezone to everyone (#3034)
This commit is contained in:
1 parent
b30ea361d3
commit
d433a039b5
28 files changed
+71
-971
No files matched your search
@@ -2,11 +2,11 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {RestResponse} from '@app/features/platform/types/TransportTypes';
|
||||
import type {ProfileTimezoneAssignmentResponse} from '@fluxer/schema/src/domains/admin/ProfileTimezoneSchemas';
|
||||
import type {DomainMigrationAssignmentResponse} from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas';
|
||||
import {
|
||||
type ExperimentAssignmentsResponse,
|
||||
INERT_EXPERIMENT_ASSIGNMENTS_RESPONSE,
|
||||
readProfileTimezoneAssignment,
|
||||
readDomainMigrationAssignment,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
@@ -26,14 +26,14 @@ vi.mock('@app/features/platform/transport/RestTransport', () => ({
|
||||
const {http} = await import('@app/features/platform/transport/RestTransport');
|
||||
const {ExperimentAssignments} = await import('@app/features/experiment/state/ExperimentAssignments');
|
||||
|
||||
const CANARY_ASSIGNMENT: ProfileTimezoneAssignmentResponse = {
|
||||
const CANARY_ASSIGNMENT: DomainMigrationAssignmentResponse = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const CANARY_ENVELOPE: ExperimentAssignmentsResponse = {
|
||||
poll_interval_seconds: 300,
|
||||
poll_jitter_percent: 15,
|
||||
assignments: {profile_timezone: CANARY_ASSIGNMENT},
|
||||
assignments: {domain_migration: CANARY_ASSIGNMENT},
|
||||
};
|
||||
|
||||
let visibility: DocumentVisibilityState = 'visible';
|
||||
@@ -68,8 +68,8 @@ function subscribe(listener: () => void): () => void {
|
||||
return unsubscribe;
|
||||
}
|
||||
|
||||
function timezoneEnabled(): boolean {
|
||||
return readProfileTimezoneAssignment(ExperimentAssignments.response).enabled;
|
||||
function migrationEnabled(): boolean {
|
||||
return readDomainMigrationAssignment(ExperimentAssignments.response).enabled;
|
||||
}
|
||||
|
||||
function deferredReply(): {resolve: (response: RestResponse<unknown>) => void} {
|
||||
@@ -111,7 +111,7 @@ describe('ExperimentAssignments cold start', () => {
|
||||
ExperimentAssignments.start();
|
||||
await settle();
|
||||
expect(ExperimentAssignments.response).toBe(INERT_EXPERIMENT_ASSIGNMENTS_RESPONSE);
|
||||
expect(ExperimentAssignments.response.assignments.profile_timezone).toBeUndefined();
|
||||
expect(ExperimentAssignments.response.assignments.domain_migration).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps the inert envelope while unauthenticated and retries later', async () => {
|
||||
@@ -129,7 +129,7 @@ describe('ExperimentAssignments response handling', () => {
|
||||
it('adopts a valid envelope', async () => {
|
||||
await adopt(CANARY_ENVELOPE);
|
||||
expect(ExperimentAssignments.response).toEqual(CANARY_ENVELOPE);
|
||||
expect(readProfileTimezoneAssignment(ExperimentAssignments.response)).toEqual(CANARY_ASSIGNMENT);
|
||||
expect(readDomainMigrationAssignment(ExperimentAssignments.response)).toEqual(CANARY_ASSIGNMENT);
|
||||
});
|
||||
|
||||
it('requests the shared experiment endpoint', async () => {
|
||||
@@ -142,7 +142,7 @@ describe('ExperimentAssignments response handling', () => {
|
||||
vi.mocked(http.get).mockResolvedValue(
|
||||
reply(200, {
|
||||
...CANARY_ENVELOPE,
|
||||
assignments: {profile_timezone: {enabled: 'yes'}},
|
||||
assignments: {domain_migration: {enabled: 'yes'}},
|
||||
}),
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(400_000);
|
||||
@@ -157,9 +157,9 @@ describe('ExperimentAssignments response handling', () => {
|
||||
expect(ExperimentAssignments.response).toEqual(CANARY_ENVELOPE);
|
||||
});
|
||||
|
||||
it('accepts an envelope that carries no profile timezone assignment', async () => {
|
||||
it('accepts an envelope that carries no domain migration assignment', async () => {
|
||||
await adopt({poll_interval_seconds: 600, poll_jitter_percent: 0, assignments: {}});
|
||||
expect(ExperimentAssignments.response.assignments.profile_timezone).toBeUndefined();
|
||||
expect(ExperimentAssignments.response.assignments.domain_migration).toBeUndefined();
|
||||
expect(lastScheduledDelayMs()).toBe(600_000);
|
||||
});
|
||||
|
||||
@@ -416,14 +416,14 @@ describe('ExperimentAssignments lifecycle', () => {
|
||||
describe('ExperimentAssignments subscribe', () => {
|
||||
it('fires once per envelope change and stops after unsubscribe', async () => {
|
||||
const seen: Array<boolean> = [];
|
||||
const unsubscribe = subscribe(() => seen.push(timezoneEnabled()));
|
||||
const unsubscribe = subscribe(() => seen.push(migrationEnabled()));
|
||||
await adopt(CANARY_ENVELOPE);
|
||||
expect(seen).toEqual([true]);
|
||||
vi.mocked(http.get).mockResolvedValue(reply(304, undefined));
|
||||
await vi.advanceTimersByTimeAsync(400_000);
|
||||
expect(seen).toEqual([true]);
|
||||
vi.mocked(http.get).mockResolvedValue(
|
||||
reply(200, {...CANARY_ENVELOPE, assignments: {profile_timezone: {enabled: false}}}, {etag: 'W/"v8"'}),
|
||||
reply(200, {...CANARY_ENVELOPE, assignments: {domain_migration: {enabled: false}}}, {etag: 'W/"v8"'}),
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(400_000);
|
||||
expect(seen).toEqual([true, false]);
|
||||
|
||||
@@ -53,7 +53,6 @@ import {ProfileTypeSelector} from '@app/features/user/components/modals/tabs/my_
|
||||
import {TimezoneProfileSettings} from '@app/features/user/components/modals/tabs/my_profile_tab/TimezoneProfileSettings';
|
||||
import {ProfilePreview} from '@app/features/user/components/profile/ProfilePreview';
|
||||
import type {Profile} from '@app/features/user/models/Profile';
|
||||
import ProfileTimezoneRollout from '@app/features/user/state/ProfileTimezoneRollout';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
|
||||
import {setMeaningfulFormValue} from '@app/lib/forms/MeaningfulFormValue';
|
||||
@@ -411,7 +410,6 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
|
||||
);
|
||||
const showPremiumFeatures = shouldShowPremiumFeatures();
|
||||
const hasPremium = useMemo(() => showPremiumFeatures && (user?.isPremium() ?? false), [showPremiumFeatures, user]);
|
||||
const hasProfileTimezoneAccess = ProfileTimezoneRollout.enabled;
|
||||
const hasPerGuildProfiles = useMemo(
|
||||
() =>
|
||||
isLimitToggleEnabled(
|
||||
@@ -537,10 +535,8 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
|
||||
};
|
||||
assignProfileAssetUploadPatch(updateData, 'avatar', avatarAsset);
|
||||
assignProfileAssetUploadPatch(updateData, 'banner', bannerAsset);
|
||||
if (hasProfileTimezoneAccess) {
|
||||
updateData.timezone = data.timezone;
|
||||
updateData.timezone_privacy_flags = data.timezone_privacy_flags;
|
||||
}
|
||||
updateData.timezone = data.timezone;
|
||||
updateData.timezone_privacy_flags = data.timezone_privacy_flags;
|
||||
if (data.premium_badge_hidden !== undefined) {
|
||||
updateData.premium_badge_hidden = data.premium_badge_hidden;
|
||||
}
|
||||
@@ -591,7 +587,6 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
|
||||
commitProfileFormValues,
|
||||
isPerGuildProfile,
|
||||
isProfileCustomizationLocked,
|
||||
hasProfileTimezoneAccess,
|
||||
selectedGuildId,
|
||||
user,
|
||||
activeProfileData,
|
||||
@@ -701,12 +696,10 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
|
||||
!bannerAsset.hasCleared &&
|
||||
(bannerAsset.hasAsset || (!bannerAsset.isDirty && Boolean(profileRemoteValues?.banner.hasCustomAsset)));
|
||||
const profileFallbackDisplayName = NicknameUtils.getDisplayName(user);
|
||||
const watchedTimezone = hasProfileTimezoneAccess ? (form.watch('timezone') ?? null) : null;
|
||||
const watchedTimezonePrivacyFlags = hasProfileTimezoneAccess
|
||||
? (form.watch('timezone_privacy_flags') ?? ProfileFieldPrivacyFlags.EVERYONE)
|
||||
: ProfileFieldPrivacyFlags.EVERYONE;
|
||||
const watchedTimezone = form.watch('timezone') ?? null;
|
||||
const watchedTimezonePrivacyFlags = form.watch('timezone_privacy_flags') ?? ProfileFieldPrivacyFlags.EVERYONE;
|
||||
const previewTimezoneOffset =
|
||||
hasProfileTimezoneAccess && !isPerGuildProfile && watchedTimezone !== null && watchedTimezonePrivacyFlags !== 0
|
||||
!isPerGuildProfile && watchedTimezone !== null && watchedTimezonePrivacyFlags !== 0
|
||||
? getCurrentTimeZoneOffsetMinutes(watchedTimezone)
|
||||
: null;
|
||||
return (
|
||||
@@ -756,7 +749,7 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
|
||||
>
|
||||
{isPerGuildProfile
|
||||
? i18n._(VERIFY_YOUR_EMAIL_BEFORE_CHANGING_YOUR_COMMUNITY_NICKNAME_DESCRIPTOR)
|
||||
: hasProfileTimezoneAccess && showPremiumFeatures
|
||||
: showPremiumFeatures
|
||||
? i18n._(VERIFY_YOUR_EMAIL_BEFORE_CHANGING_YOUR_USERNAME_DISPLAY_DESCRIPTOR, {
|
||||
premiumProductName: PREMIUM_PRODUCT_NAME,
|
||||
})
|
||||
@@ -832,7 +825,7 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
|
||||
disabled={isPronounsDisabled}
|
||||
/>
|
||||
</div>
|
||||
{!isPerGuildProfile && hasProfileTimezoneAccess && (
|
||||
{!isPerGuildProfile && (
|
||||
<TimezoneProfileSettings
|
||||
timezone={watchedTimezone}
|
||||
timezonePrivacyFlags={watchedTimezonePrivacyFlags}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import ExperimentAssignments from '@app/features/experiment/state/ExperimentAssignments';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {readProfileTimezoneAssignment} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
|
||||
const logger = new Logger('ProfileTimezoneRollout');
|
||||
|
||||
class ProfileTimezoneRolloutSelector {
|
||||
get enabled(): boolean {
|
||||
try {
|
||||
return readProfileTimezoneAssignment(ExperimentAssignments.response).enabled;
|
||||
} catch (err) {
|
||||
logger.warn('Failed to resolve profile timezone assignment:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ProfileTimezoneRollout = new ProfileTimezoneRolloutSelector();
|
||||
|
||||
export default ProfileTimezoneRollout;
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import {Profile} from '@app/features/user/models/Profile';
|
||||
import type {User} from '@app/features/user/models/User';
|
||||
import ProfileTimezoneRollout from '@app/features/user/state/ProfileTimezoneRollout';
|
||||
import {ProfileFieldPrivacyFlags, UserPremiumTypes} from '@fluxer/constants/src/UserConstants';
|
||||
import {getCurrentTimeZoneOffsetMinutes} from '@fluxer/date_utils/src/TimeZoneUtils';
|
||||
|
||||
@@ -97,7 +96,7 @@ export function createMockProfile(
|
||||
timezone_offset:
|
||||
options?.previewTimezoneOffset !== undefined
|
||||
? options.previewTimezoneOffset
|
||||
: ProfileTimezoneRollout.enabled && (user.timezonePrivacyFlags ?? ProfileFieldPrivacyFlags.EVERYONE) !== 0
|
||||
: (user.timezonePrivacyFlags ?? ProfileFieldPrivacyFlags.EVERYONE) !== 0
|
||||
? getCurrentTimeZoneOffsetMinutes(user.timezone)
|
||||
: null,
|
||||
premium_type: visiblePremiumData.premiumType ?? undefined,
|
||||
|
||||
Reference in new issue
Block a user