Merge branch 'chore/improve-conversation-snooze' into chore/improve-custom-snooze

This commit is contained in:
Sivin Varghese
2026-02-23 18:25:06 +05:30
committed by GitHub
17 changed files with 1930 additions and 497 deletions
File diff suppressed because it is too large Load Diff
@@ -35,89 +35,93 @@ export const findStartOfNextMonth = currentDate => {
});
};
export const findNextDay = currentDate => {
return add(currentDate, { days: 1 });
};
export const findNextDay = currentDate => add(currentDate, { days: 1 });
export const setHoursToNine = date => {
return setSeconds(setMinutes(setHours(date, 9), 0), 0);
export const setHoursToNine = date =>
setSeconds(setMinutes(setHours(date, 9), 0), 0);
const SNOOZE_RESOLVERS = {
[SNOOZE_OPTIONS.AN_HOUR_FROM_NOW]: d => add(d, { hours: 1 }),
[SNOOZE_OPTIONS.UNTIL_TOMORROW]: d => setHoursToNine(findNextDay(d)),
[SNOOZE_OPTIONS.UNTIL_NEXT_WEEK]: d => setHoursToNine(findStartOfNextWeek(d)),
[SNOOZE_OPTIONS.UNTIL_NEXT_MONTH]: d =>
setHoursToNine(findStartOfNextMonth(d)),
};
export const findSnoozeTime = (snoozeType, currentDate = new Date()) => {
let parsedDate = null;
if (snoozeType === SNOOZE_OPTIONS.AN_HOUR_FROM_NOW) {
parsedDate = add(currentDate, { hours: 1 });
} else if (snoozeType === SNOOZE_OPTIONS.UNTIL_TOMORROW) {
parsedDate = setHoursToNine(findNextDay(currentDate));
} else if (snoozeType === SNOOZE_OPTIONS.UNTIL_NEXT_WEEK) {
parsedDate = setHoursToNine(findStartOfNextWeek(currentDate));
} else if (snoozeType === SNOOZE_OPTIONS.UNTIL_NEXT_MONTH) {
parsedDate = setHoursToNine(findStartOfNextMonth(currentDate));
}
return parsedDate ? getUnixTime(parsedDate) : null;
const resolve = SNOOZE_RESOLVERS[snoozeType];
return resolve ? getUnixTime(resolve(currentDate)) : null;
};
export const snoozedReopenTime = snoozedUntil => {
if (!snoozedUntil) {
return null;
}
if (!snoozedUntil) return null;
const date = new Date(snoozedUntil);
if (isToday(date)) {
return format(date, 'h.mmaaa');
}
if (!isSameYear(date, new Date())) {
return format(date, 'd MMM yyyy, h.mmaaa');
}
return snoozedUntil ? format(date, 'd MMM, h.mmaaa') : null;
if (isToday(date)) return format(date, 'h.mmaaa');
if (!isSameYear(date, new Date())) return format(date, 'd MMM yyyy, h.mmaaa');
return format(date, 'd MMM, h.mmaaa');
};
export const snoozedReopenTimeToTimestamp = snoozedUntil => {
return snoozedUntil ? getUnixTime(new Date(snoozedUntil)) : null;
};
const formatSnoozeDate = (snoozeDate, currentDate) => {
return isSameYear(snoozeDate, currentDate)
? format(snoozeDate, 'EEE, d MMM, h:mm a')
: format(snoozeDate, 'EEE, d MMM yyyy, h:mm a');
export const snoozedReopenTimeToTimestamp = snoozedUntil =>
snoozedUntil ? getUnixTime(new Date(snoozedUntil)) : null;
const formatSnoozeDate = (snoozeDate, currentDate, locale = 'en') => {
const sameYear = isSameYear(snoozeDate, currentDate);
try {
const opts = {
weekday: 'short',
day: 'numeric',
month: 'short',
hour: 'numeric',
minute: '2-digit',
hour12: true,
...(sameYear ? {} : { year: 'numeric' }),
};
return new Intl.DateTimeFormat(locale, opts).format(snoozeDate);
} catch {
return sameYear
? format(snoozeDate, 'EEE, d MMM, h:mm a')
: format(snoozeDate, 'EEE, d MMM yyyy, h:mm a');
}
};
const capitalizeLabel = text => text.replace(/^\w/, c => c.toUpperCase());
export const generateSnoozeSuggestions = (
searchText,
currentDate = new Date()
currentDate = new Date(),
{ translations, locale } = {}
) => {
const suggestions = generateDateSuggestions(searchText, currentDate);
const suggestions = generateDateSuggestions(searchText, currentDate, {
translations,
locale,
});
return suggestions.map(s => ({
date: s.date,
unixTime: s.unix,
label: capitalizeLabel(s.label),
formattedDate: formatSnoozeDate(s.date, currentDate),
formattedDate: formatSnoozeDate(s.date, currentDate, locale),
}));
};
const UNIT_SHORT = {
minute: 'm',
minutes: 'm',
hour: 'h',
hours: 'h',
day: 'd',
days: 'd',
month: 'mo',
months: 'mo',
year: 'y',
years: 'y',
};
export const shortenSnoozeTime = snoozedUntil => {
if (!snoozedUntil) {
return null;
}
const unitMap = {
minutes: 'm',
minute: 'm',
hours: 'h',
hour: 'h',
days: 'd',
day: 'd',
months: 'mo',
month: 'mo',
years: 'y',
year: 'y',
};
const shortenTime = snoozedUntil
if (!snoozedUntil) return null;
return snoozedUntil
.replace(/^in\s+/i, '')
.replace(
/\s(minute|hour|day|month|year)s?\b/gi,
(match, unit) => unitMap[unit.toLowerCase()] || match
(match, unit) => UNIT_SHORT[unit.toLowerCase()] || match
);
return shortenTime;
};
@@ -816,6 +816,387 @@ describe('generateDateSuggestions', () => {
expect(results.length).toBeLessThanOrEqual(5);
});
});
describe('smart compositional suggestions', () => {
it('"mon" suggests monday + time-of-day variants (noon, afternoon, evening, night)', () => {
const results = generateDateSuggestions('mon', now);
const labels = results.map(r => r.label);
// "monday morning" (9am) is deduped with "monday" (default 9am), so noon+ appear
expect(labels.some(l => /monday\s+afternoon/.test(l))).toBe(true);
expect(labels.some(l => /monday\s+evening/.test(l))).toBe(true);
});
it('"monday" suggests multiple time-of-day variants', () => {
const results = generateDateSuggestions('monday', now);
const labels = results.map(r => r.label);
expect(labels.some(l => l.includes('monday afternoon'))).toBe(true);
expect(labels.some(l => l.includes('monday evening'))).toBe(true);
expect(results.length).toBeGreaterThanOrEqual(3);
});
it('"fri" suggests friday + time-of-day variants', () => {
const results = generateDateSuggestions('fri', now);
const labels = results.map(r => r.label);
expect(labels.some(l => /friday/.test(l))).toBe(true);
expect(results.length).toBeGreaterThanOrEqual(3);
});
it('"tomorrow m" suggests tomorrow morning', () => {
const results = generateDateSuggestions('tomorrow m', now);
const labels = results.map(r => r.label);
expect(labels.some(l => l.includes('tomorrow morning'))).toBe(true);
});
it('"tomorrow a" suggests tomorrow afternoon', () => {
const results = generateDateSuggestions('tomorrow a', now);
const labels = results.map(r => r.label);
expect(labels.some(l => l.includes('tomorrow afternoon'))).toBe(true);
});
it('"next mon" suggests next monday and next month', () => {
const results = generateDateSuggestions('next mon', now);
const labels = results.map(r => r.label);
expect(labels.some(l => l.includes('next mon'))).toBe(true);
});
it('"next monday m" suggests next monday morning', () => {
const results = generateDateSuggestions('next monday m', now);
const labels = results.map(r => r.label);
expect(labels.some(l => l.includes('next monday morning'))).toBe(true);
});
it('"t" suggests today, tonight, tomorrow', () => {
const results = generateDateSuggestions('t', now);
const labels = results.map(r => r.label);
expect(
labels.some(l => l === 'today' || l === 'tonight' || l === 'tomorrow')
).toBe(true);
});
it('"n" suggests next week, next month, next weekdays', () => {
const results = generateDateSuggestions('n', now);
const labels = results.map(r => r.label);
expect(labels.some(l => l.includes('next'))).toBe(true);
});
it('all suggestions parse to valid future dates', () => {
const inputs = ['mon', 'monday', 'fri', 'tomorrow m', 'next mon', 't'];
inputs.forEach(input => {
const results = generateDateSuggestions(input, now);
results.forEach(r => {
expect(r.date).toBeInstanceOf(Date);
expect(r.date > now).toBe(true);
expect(typeof r.unix).toBe('number');
});
});
});
});
});
describe('bare number + time-of-day context inference', () => {
it('"morning 6" parses to 6am', () => {
const result = parseDateFromText('morning 6', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(6);
});
it('"evening 7" parses to 7pm (19:00)', () => {
const result = parseDateFromText('evening 7', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(19);
});
it('"afternoon 3" parses to 3pm (15:00)', () => {
const result = parseDateFromText('afternoon 3', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(15);
});
it('"night 9" parses to 9pm (21:00)', () => {
const result = parseDateFromText('night 9', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(21);
});
it('"tomorrow morning 6" parses to tomorrow 6am', () => {
const result = parseDateFromText('tomorrow morning 6', now);
expect(result).not.toBeNull();
expect(result.date.getDate()).toBe(17);
expect(result.date.getHours()).toBe(6);
});
it('"tomorrow evening 7" parses to tomorrow 7pm', () => {
const result = parseDateFromText('tomorrow evening 7', now);
expect(result).not.toBeNull();
expect(result.date.getDate()).toBe(17);
expect(result.date.getHours()).toBe(19);
});
it('"monday morning 6" parses to next monday 6am', () => {
const result = parseDateFromText('monday morning 6', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(6);
});
it('"friday evening 8" parses to friday 8pm', () => {
const result = parseDateFromText('friday evening 8', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(20);
});
it('explicit meridiem still works: "morning 6am" → 6am', () => {
const result = parseDateFromText('morning 6am', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(6);
});
it('contradictory meridiem still rejected: "morning 7pm" → null', () => {
expect(parseDateFromText('morning 7pm', now)).toBeNull();
});
});
describe('localized suggestions with Malayalam translations', () => {
const mlTranslations = {
UNITS: {
MINUTE: 'മിനിറ്റ്',
MINUTES: 'മിനിറ്റ്',
HOUR: 'മണിക്കൂർ',
HOURS: 'മണിക്കൂർ',
DAY: 'ദിവസം',
DAYS: 'ദിവസം',
WEEK: 'ആഴ്ച',
WEEKS: 'ആഴ്ച',
MONTH: 'മാസം',
MONTHS: 'മാസം',
YEAR: 'വർഷം',
YEARS: 'വർഷം',
},
HALF: 'അര',
NEXT: 'അടുത്ത',
THIS: 'ഈ',
AT: 'സമയം',
IN: 'കഴിഞ്ഞ്',
FROM_NOW: 'ഇപ്പോൾ മുതൽ',
NEXT_YEAR: 'അടുത്ത വർഷം',
MERIDIEM: { AM: 'രാവിലെ', PM: 'വൈകുന്നേരം' },
RELATIVE: {
TOMORROW: 'നാളെ',
DAY_AFTER_TOMORROW: 'മറ്റന്നാൾ',
NEXT_WEEK: 'അടുത്ത ആഴ്ച',
NEXT_MONTH: 'അടുത്ത മാസം',
THIS_WEEKEND: 'ഈ വാരാന്ത്യം',
NEXT_WEEKEND: 'അടുത്ത വാരാന്ത്യം',
},
TIME_OF_DAY: {
MORNING: 'രാവിലെ',
AFTERNOON: 'ഉച്ചയ്ക്ക്',
EVENING: 'വൈകുന്നേരം',
NIGHT: 'രാത്രി',
NOON: 'ഉച്ച',
MIDNIGHT: 'അർദ്ധരാത്രി',
},
WORD_NUMBERS: {
ONE: 'ഒന്ന്',
TWO: 'രണ്ട്',
THREE: 'മൂന്ന്',
FOUR: 'നാല്',
FIVE: 'അഞ്ച്',
SIX: 'ആറ്',
SEVEN: 'ഏഴ്',
EIGHT: 'എട്ട്',
NINE: 'ഒൻപത്',
TEN: 'പത്ത്',
TWELVE: 'പന്ത്രണ്ട്',
FIFTEEN: 'പതിനഞ്ച്',
TWENTY: 'ഇരുപത്',
THIRTY: 'മുപ്പത്',
},
};
it('Malayalam "നാളെ രാവിലെ 6" parses to tomorrow 6am', () => {
const results = generateDateSuggestions('നാളെ രാവിലെ 6', now, {
translations: mlTranslations,
locale: 'ml',
});
expect(results.length).toBeGreaterThan(0);
expect(results[0].date.getDate()).toBe(17);
expect(results[0].date.getHours()).toBe(6);
});
it('Malayalam "നാളെ" (tomorrow) generates multiple suggestions', () => {
const results = generateDateSuggestions('നാളെ', now, {
translations: mlTranslations,
locale: 'ml',
});
expect(results.length).toBeGreaterThanOrEqual(3);
expect(results[0].date.getDate()).toBe(17);
});
it('Malayalam suggestion labels are in Malayalam, not English', () => {
const results = generateDateSuggestions('നാളെ', now, {
translations: mlTranslations,
locale: 'ml',
});
const labels = results.map(r => r.label);
expect(labels.some(l => //.test(l))).toBe(true);
expect(labels.every(l => !/\btomorrow\b/.test(l))).toBe(true);
});
});
describe('chrono-level patterns', () => {
describe('tomorrow at TOD', () => {
it('"tomorrow at noon" parses to tomorrow 12pm', () => {
const result = parseDateFromText('tomorrow at noon', now);
expect(result).not.toBeNull();
expect(result.date.getDate()).toBe(17);
expect(result.date.getHours()).toBe(12);
});
it('"tomorrow at midnight" parses to tomorrow 0am', () => {
const result = parseDateFromText('tomorrow at midnight', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(0);
});
it('"tomorrow at evening" parses to tomorrow 6pm', () => {
const result = parseDateFromText('tomorrow at evening', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(18);
});
});
describe('duration at time', () => {
it('"in 2 days at 3pm" parses correctly', () => {
const result = parseDateFromText('in 2 days at 3pm', now);
expect(result).not.toBeNull();
expect(result.date.getDate()).toBe(18);
expect(result.date.getHours()).toBe(15);
});
it('"in 1 week at 9am" parses correctly', () => {
const result = parseDateFromText('in 1 week at 9am', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(9);
});
});
describe('end of period', () => {
it('"end of day" parses to today 5pm', () => {
const result = parseDateFromText('end of day', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(17);
});
it('"end of the week" parses to next friday 5pm', () => {
const result = parseDateFromText('end of the week', now);
expect(result).not.toBeNull();
expect(result.date.getDay()).toBe(5);
expect(result.date.getHours()).toBe(17);
});
it('"end of month" parses to last day of month 5pm', () => {
const result = parseDateFromText('end of month', now);
expect(result).not.toBeNull();
expect(result.date.getDate()).toBe(30);
expect(result.date.getHours()).toBe(17);
});
it('"end of month" on last day after 5pm rolls to next month-end', () => {
const lastDayLate = new Date('2025-06-30T18:00:00');
const result = parseDateFromText('end of month', lastDayLate);
expect(result).not.toBeNull();
expect(result.date.getMonth()).toBe(6);
expect(result.date.getDate()).toBe(31);
expect(result.date.getHours()).toBe(17);
});
});
describe('later today', () => {
it('"later today" parses to +3 hours from now', () => {
const result = parseDateFromText('later today', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(13);
});
});
describe('compound durations', () => {
it('"1 hour 30 minutes" parses correctly', () => {
const result = parseDateFromText('1 hour 30 minutes', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(11);
expect(result.date.getMinutes()).toBe(30);
});
it('"1h30m" parses correctly', () => {
const result = parseDateFromText('1h30m', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(11);
expect(result.date.getMinutes()).toBe(30);
});
it('"2 hours and 30 minutes" parses correctly', () => {
const result = parseDateFromText('2 hours and 30 minutes', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(12);
expect(result.date.getMinutes()).toBe(30);
});
});
describe('aliases and shortcuts', () => {
it('"tonite" parses to tonight (8pm)', () => {
const result = parseDateFromText('tonite', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(20);
});
it('"couple hours" parses to +2 hours', () => {
const result = parseDateFromText('couple hours', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(12);
});
it('"couple of hours" parses to +2 hours', () => {
const result = parseDateFromText('couple of hours', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(12);
});
it('"few hours" parses to +3 hours', () => {
const result = parseDateFromText('few hours', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(13);
});
it('"nxt week" parses like "next week"', () => {
const result = parseDateFromText('nxt week', now);
expect(result).not.toBeNull();
});
it('"nxt monday" parses like "next monday"', () => {
const result = parseDateFromText('nxt monday', now);
expect(result).not.toBeNull();
});
});
describe('weekday bare hour defaults to PM', () => {
it('"monday at 3" parses to 3pm', () => {
const result = parseDateFromText('monday at 3', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(15);
});
it('"friday at 5" parses to 5pm', () => {
const result = parseDateFromText('friday at 5', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(17);
});
it('"monday at 9" stays 9am (hour >= 8)', () => {
const result = parseDateFromText('monday at 9', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toBe(9);
});
});
});
describe('dot-delimited dates', () => {
@@ -918,3 +1299,217 @@ describe('decimal duration parsing (only .5 allowed)', () => {
expect(parseDateFromText('2.7 days', now)).toBeNull();
});
});
// ─── Multilingual / Localized Input Regressions ─────────────────────────────
describe('generateDateSuggestions — localized input regressions', () => {
const arTranslations = {
UNITS: {
MINUTE: 'دقيقة',
MINUTES: 'دقائق',
HOUR: 'ساعة',
HOURS: 'ساعات',
DAY: 'يوم',
DAYS: 'أيام',
WEEK: 'أسبوع',
WEEKS: 'أسابيع',
MONTH: 'شهر',
MONTHS: 'أشهر',
YEAR: 'سنة',
YEARS: 'سنوات',
},
HALF: 'نصف',
NEXT: 'القادم',
THIS: 'هذا',
AT: 'الساعة',
IN: 'في',
FROM_NOW: 'من الآن',
NEXT_YEAR: 'العام المقبل',
MERIDIEM: { AM: 'صباحاً', PM: 'مساءً' },
RELATIVE: {
TOMORROW: 'غداً',
DAY_AFTER_TOMORROW: 'بعد غد',
NEXT_WEEK: 'الأسبوع القادم',
NEXT_MONTH: 'الشهر القادم',
THIS_WEEKEND: 'نهاية هذا الأسبوع',
NEXT_WEEKEND: 'نهاية الأسبوع القادم',
},
TIME_OF_DAY: {
MORNING: 'صباحاً',
AFTERNOON: 'بعد الظهر',
EVENING: 'مساءً',
NIGHT: 'ليلاً',
NOON: 'ظهراً',
MIDNIGHT: 'منتصف الليل',
},
WORD_NUMBERS: {
ONE: 'واحد',
TWO: 'اثنان',
THREE: 'ثلاثة',
FOUR: 'أربعة',
FIVE: 'خمسة',
SIX: 'ستة',
SEVEN: 'سبعة',
EIGHT: 'ثمانية',
NINE: 'تسعة',
TEN: 'عشرة',
TWELVE: 'اثنا عشر',
FIFTEEN: 'خمسة عشر',
TWENTY: 'عشرون',
THIRTY: 'ثلاثون',
},
};
const hiTranslations = {
UNITS: {
MINUTE: 'मिनट',
MINUTES: 'मिनट',
HOUR: 'घंटा',
HOURS: 'घंटे',
DAY: 'दिन',
DAYS: 'दिन',
WEEK: 'सप्ताह',
WEEKS: 'सप्ताह',
MONTH: 'महीना',
MONTHS: 'महीने',
YEAR: 'साल',
YEARS: 'साल',
},
HALF: 'आधा',
NEXT: 'अगला',
THIS: 'यह',
AT: 'बजे',
IN: 'में',
FROM_NOW: 'अब से',
NEXT_YEAR: 'अगले साल',
MERIDIEM: { AM: 'सुबह', PM: 'शाम' },
RELATIVE: {
TOMORROW: 'कल',
DAY_AFTER_TOMORROW: 'परसों',
NEXT_WEEK: 'अगले सप्ताह',
NEXT_MONTH: 'अगले महीने',
THIS_WEEKEND: 'इस सप्ताहांत',
NEXT_WEEKEND: 'अगले सप्ताहांत',
},
TIME_OF_DAY: {
MORNING: 'सुबह',
AFTERNOON: 'दोपहर',
EVENING: 'शाम',
NIGHT: 'रात',
NOON: 'दोपहर',
MIDNIGHT: 'आधी रात',
},
WORD_NUMBERS: {
ONE: 'एक',
TWO: 'दो',
THREE: 'तीन',
FOUR: 'चार',
FIVE: 'पाँच',
SIX: 'छह',
SEVEN: 'सात',
EIGHT: 'आठ',
NINE: 'नौ',
TEN: 'दस',
TWELVE: 'बारह',
FIFTEEN: 'पंद्रह',
TWENTY: 'बीस',
THIRTY: 'तीस',
},
};
describe('P1: short non-English tokens must NOT produce spurious half-duration suggestions', () => {
it('Arabic "غد" does not produce half-duration suggestions', () => {
const results = generateDateSuggestions('غد', now, {
translations: arTranslations,
locale: 'ar',
});
const halfLabels = results.filter(r => /half/i.test(r.label));
expect(halfLabels).toHaveLength(0);
});
it('Hindi "सु" does not produce half-duration suggestions', () => {
const results = generateDateSuggestions('सु', now, {
translations: hiTranslations,
locale: 'hi',
});
const halfLabels = results.filter(r => /half/i.test(r.label));
expect(halfLabels).toHaveLength(0);
});
});
describe('P1: MERIDIEM vs TIME_OF_DAY — "tomorrow morning" must parse in locales where AM = morning', () => {
it('Arabic "غداً صباحاً" (tomorrow morning) parses correctly', () => {
const results = generateDateSuggestions('غداً صباحاً', now, {
translations: arTranslations,
locale: 'ar',
});
expect(results.length).toBeGreaterThan(0);
const first = results[0];
expect(first.date.getDate()).toBe(17);
expect(first.date.getHours()).toBe(9);
});
it('Hindi "कल सुबह" (tomorrow morning) parses correctly', () => {
const results = generateDateSuggestions('कल सुबह', now, {
translations: hiTranslations,
locale: 'hi',
});
expect(results.length).toBeGreaterThan(0);
const first = results[0];
expect(first.date.getDate()).toBe(17);
expect(first.date.getHours()).toBe(9);
});
});
describe('basic localized parsing still works', () => {
it('Arabic "غداً" (tomorrow) parses to tomorrow 9am', () => {
const results = generateDateSuggestions('غداً', now, {
translations: arTranslations,
locale: 'ar',
});
expect(results.length).toBeGreaterThan(0);
expect(results[0].date.getDate()).toBe(17);
});
it('Hindi "कल" (tomorrow) parses to tomorrow', () => {
const results = generateDateSuggestions('कल', now, {
translations: hiTranslations,
locale: 'hi',
});
expect(results.length).toBeGreaterThan(0);
expect(results[0].date.getDate()).toBe(17);
});
it('Arabic "غداً،" (tomorrow with attached Arabic comma) parses correctly', () => {
const results = generateDateSuggestions('غداً،', now, {
translations: arTranslations,
locale: 'ar',
});
expect(results.length).toBeGreaterThan(0);
expect(results[0].date.getDate()).toBe(17);
});
});
describe('localized Unicode digits', () => {
it('Arabic-Indic digits parse in time expressions', () => {
const results = generateDateSuggestions('غداً الساعة ١٢:٣٠', now, {
translations: arTranslations,
locale: 'ar',
});
expect(results.length).toBeGreaterThan(0);
expect(results[0].date.getDate()).toBe(17);
expect(results[0].date.getHours()).toBe(12);
expect(results[0].date.getMinutes()).toBe(30);
});
it('Devanagari digits parse in time-of-day expressions', () => {
const results = generateDateSuggestions('कल सुबह ६', now, {
translations: hiTranslations,
locale: 'hi',
});
expect(results.length).toBeGreaterThan(0);
expect(results[0].date.getDate()).toBe(17);
expect(results[0].date.getHours()).toBe(6);
});
});
});
@@ -33,6 +33,7 @@ import setNewPassword from './setNewPassword.json';
import settings from './settings.json';
import signup from './signup.json';
import sla from './sla.json';
import snooze from './snooze.json';
import teamsSettings from './teamsSettings.json';
import whatsappTemplates from './whatsappTemplates.json';
@@ -72,6 +73,7 @@ export default {
...settings,
...signup,
...sla,
...snooze,
...teamsSettings,
...whatsappTemplates,
};
@@ -0,0 +1,61 @@
{
"SNOOZE_PARSER": {
"UNITS": {
"MINUTE": "دقيقة",
"MINUTES": "دقائق",
"HOUR": "ساعة",
"HOURS": "ساعات",
"DAY": "يوم",
"DAYS": "أيام",
"WEEK": "أسبوع",
"WEEKS": "أسابيع",
"MONTH": "شهر",
"MONTHS": "أشهر",
"YEAR": "سنة",
"YEARS": "سنوات"
},
"HALF": "نصف",
"NEXT": "القادم",
"THIS": "هذا",
"AT": "الساعة",
"IN": "في",
"FROM_NOW": "من الآن",
"NEXT_YEAR": "العام المقبل",
"MERIDIEM": {
"AM": "صباحاً",
"PM": "مساءً"
},
"RELATIVE": {
"TOMORROW": "غداً",
"DAY_AFTER_TOMORROW": "بعد غد",
"NEXT_WEEK": "الأسبوع القادم",
"NEXT_MONTH": "الشهر القادم",
"THIS_WEEKEND": "نهاية هذا الأسبوع",
"NEXT_WEEKEND": "نهاية الأسبوع القادم"
},
"TIME_OF_DAY": {
"MORNING": "صباحاً",
"AFTERNOON": "بعد الظهر",
"EVENING": "مساءً",
"NIGHT": "ليلاً",
"NOON": "ظهراً",
"MIDNIGHT": "منتصف الليل"
},
"WORD_NUMBERS": {
"ONE": "واحد",
"TWO": "اثنان",
"THREE": "ثلاثة",
"FOUR": "أربعة",
"FIVE": "خمسة",
"SIX": "ستة",
"SEVEN": "سبعة",
"EIGHT": "ثمانية",
"NINE": "تسعة",
"TEN": "عشرة",
"TWELVE": "اثنا عشر",
"FIFTEEN": "خمسة عشر",
"TWENTY": "عشرون",
"THIRTY": "ثلاثون"
}
}
}
@@ -34,6 +34,7 @@ import setNewPassword from './setNewPassword.json';
import settings from './settings.json';
import signup from './signup.json';
import sla from './sla.json';
import snooze from './snooze.json';
import teamsSettings from './teamsSettings.json';
import whatsappTemplates from './whatsappTemplates.json';
import contentTemplates from './contentTemplates.json';
@@ -77,6 +78,7 @@ export default {
...settings,
...signup,
...sla,
...snooze,
...teamsSettings,
...whatsappTemplates,
...contentTemplates,
@@ -0,0 +1,61 @@
{
"SNOOZE_PARSER": {
"UNITS": {
"MINUTE": "minute",
"MINUTES": "minutes",
"HOUR": "hour",
"HOURS": "hours",
"DAY": "day",
"DAYS": "days",
"WEEK": "week",
"WEEKS": "weeks",
"MONTH": "month",
"MONTHS": "months",
"YEAR": "year",
"YEARS": "years"
},
"HALF": "half",
"NEXT": "next",
"THIS": "this",
"AT": "at",
"IN": "in",
"FROM_NOW": "from now",
"NEXT_YEAR": "next year",
"MERIDIEM": {
"AM": "am",
"PM": "pm"
},
"RELATIVE": {
"TOMORROW": "tomorrow",
"DAY_AFTER_TOMORROW": "day after tomorrow",
"NEXT_WEEK": "next week",
"NEXT_MONTH": "next month",
"THIS_WEEKEND": "this weekend",
"NEXT_WEEKEND": "next weekend"
},
"TIME_OF_DAY": {
"MORNING": "morning",
"AFTERNOON": "afternoon",
"EVENING": "evening",
"NIGHT": "night",
"NOON": "noon",
"MIDNIGHT": "midnight"
},
"WORD_NUMBERS": {
"ONE": "one",
"TWO": "two",
"THREE": "three",
"FOUR": "four",
"FIVE": "five",
"SIX": "six",
"SEVEN": "seven",
"EIGHT": "eight",
"NINE": "nine",
"TEN": "ten",
"TWELVE": "twelve",
"FIFTEEN": "fifteen",
"TWENTY": "twenty",
"THIRTY": "thirty"
}
}
}
@@ -33,6 +33,7 @@ import setNewPassword from './setNewPassword.json';
import settings from './settings.json';
import signup from './signup.json';
import sla from './sla.json';
import snooze from './snooze.json';
import teamsSettings from './teamsSettings.json';
import whatsappTemplates from './whatsappTemplates.json';
@@ -72,6 +73,7 @@ export default {
...settings,
...signup,
...sla,
...snooze,
...teamsSettings,
...whatsappTemplates,
};
@@ -0,0 +1,61 @@
{
"SNOOZE_PARSER": {
"UNITS": {
"MINUTE": "minuto",
"MINUTES": "minutos",
"HOUR": "hora",
"HOURS": "horas",
"DAY": "día",
"DAYS": "días",
"WEEK": "semana",
"WEEKS": "semanas",
"MONTH": "mes",
"MONTHS": "meses",
"YEAR": "año",
"YEARS": "años"
},
"HALF": "media",
"NEXT": "próximo",
"THIS": "este",
"AT": "a las",
"IN": "en",
"FROM_NOW": "a partir de ahora",
"NEXT_YEAR": "el próximo año",
"MERIDIEM": {
"AM": "am",
"PM": "pm"
},
"RELATIVE": {
"TOMORROW": "mañana",
"DAY_AFTER_TOMORROW": "pasado mañana",
"NEXT_WEEK": "la próxima semana",
"NEXT_MONTH": "el próximo mes",
"THIS_WEEKEND": "este fin de semana",
"NEXT_WEEKEND": "el próximo fin de semana"
},
"TIME_OF_DAY": {
"MORNING": "mañana",
"AFTERNOON": "tarde",
"EVENING": "noche",
"NIGHT": "noche",
"NOON": "mediodía",
"MIDNIGHT": "medianoche"
},
"WORD_NUMBERS": {
"ONE": "uno",
"TWO": "dos",
"THREE": "tres",
"FOUR": "cuatro",
"FIVE": "cinco",
"SIX": "seis",
"SEVEN": "siete",
"EIGHT": "ocho",
"NINE": "nueve",
"TEN": "diez",
"TWELVE": "doce",
"FIFTEEN": "quince",
"TWENTY": "veinte",
"THIRTY": "treinta"
}
}
}
@@ -33,6 +33,7 @@ import setNewPassword from './setNewPassword.json';
import settings from './settings.json';
import signup from './signup.json';
import sla from './sla.json';
import snooze from './snooze.json';
import teamsSettings from './teamsSettings.json';
import whatsappTemplates from './whatsappTemplates.json';
@@ -72,6 +73,7 @@ export default {
...settings,
...signup,
...sla,
...snooze,
...teamsSettings,
...whatsappTemplates,
};
@@ -0,0 +1,58 @@
{
"SNOOZE_PARSER": {
"UNITS": {
"MINUTE": "മിനിറ്റ്",
"MINUTES": "മിനിറ്റ്",
"HOUR": "മണിക്കൂർ",
"HOURS": "മണിക്കൂർ",
"DAY": "ദിവസം",
"DAYS": "ദിവസം",
"WEEK": "ആഴ്ച",
"WEEKS": "ആഴ്ച",
"MONTH": "മാസം",
"MONTHS": "മാസം",
"YEAR": "വർഷം",
"YEARS": "വർഷം"
},
"HALF": "അര",
"NEXT": "അടുത്ത",
"THIS": "ഈ",
"AT": "സമയം",
"IN": "കഴിഞ്ഞ്",
"FROM_NOW": "ഇപ്പോൾ മുതൽ",
"NEXT_YEAR": "അടുത്ത വർഷം",
"MERIDIEM": { "AM": "രാവിലെ", "PM": "വൈകുന്നേരം" },
"RELATIVE": {
"TOMORROW": "നാളെ",
"DAY_AFTER_TOMORROW": "മറ്റന്നാൾ",
"NEXT_WEEK": "അടുത്ത ആഴ്ച",
"NEXT_MONTH": "അടുത്ത മാസം",
"THIS_WEEKEND": "ഈ വാരാന്ത്യം",
"NEXT_WEEKEND": "അടുത്ത വാരാന്ത്യം"
},
"TIME_OF_DAY": {
"MORNING": "രാവിലെ",
"AFTERNOON": "ഉച്ചയ്ക്ക്",
"EVENING": "വൈകുന്നേരം",
"NIGHT": "രാത്രി",
"NOON": "ഉച്ച",
"MIDNIGHT": "അർദ്ധരാത്രി"
},
"WORD_NUMBERS": {
"ONE": "ഒന്ന്",
"TWO": "രണ്ട്",
"THREE": "മൂന്ന്",
"FOUR": "നാല്",
"FIVE": "അഞ്ച്",
"SIX": "ആറ്",
"SEVEN": "ഏഴ്",
"EIGHT": "എട്ട്",
"NINE": "ഒൻപത്",
"TEN": "പത്ത്",
"TWELVE": "പന്ത്രണ്ട്",
"FIFTEEN": "പതിനഞ്ച്",
"TWENTY": "ഇരുപത്",
"THIRTY": "മുപ്പത്"
}
}
}
@@ -33,6 +33,7 @@ import setNewPassword from './setNewPassword.json';
import settings from './settings.json';
import signup from './signup.json';
import sla from './sla.json';
import snooze from './snooze.json';
import teamsSettings from './teamsSettings.json';
import whatsappTemplates from './whatsappTemplates.json';
@@ -72,6 +73,7 @@ export default {
...settings,
...signup,
...sla,
...snooze,
...teamsSettings,
...whatsappTemplates,
};
@@ -0,0 +1,61 @@
{
"SNOOZE_PARSER": {
"UNITS": {
"MINUTE": "minuto",
"MINUTES": "minutos",
"HOUR": "hora",
"HOURS": "horas",
"DAY": "dia",
"DAYS": "dias",
"WEEK": "semana",
"WEEKS": "semanas",
"MONTH": "mês",
"MONTHS": "meses",
"YEAR": "ano",
"YEARS": "anos"
},
"HALF": "meia",
"NEXT": "próximo",
"THIS": "este",
"AT": "às",
"IN": "em",
"FROM_NOW": "a partir de agora",
"NEXT_YEAR": "próximo ano",
"MERIDIEM": {
"AM": "am",
"PM": "pm"
},
"RELATIVE": {
"TOMORROW": "amanhã",
"DAY_AFTER_TOMORROW": "depois de amanhã",
"NEXT_WEEK": "próxima semana",
"NEXT_MONTH": "próximo mês",
"THIS_WEEKEND": "este fim de semana",
"NEXT_WEEKEND": "próximo fim de semana"
},
"TIME_OF_DAY": {
"MORNING": "manhã",
"AFTERNOON": "tarde",
"EVENING": "noite",
"NIGHT": "noite",
"NOON": "meio-dia",
"MIDNIGHT": "meia-noite"
},
"WORD_NUMBERS": {
"ONE": "um",
"TWO": "dois",
"THREE": "três",
"FOUR": "quatro",
"FIVE": "cinco",
"SIX": "seis",
"SEVEN": "sete",
"EIGHT": "oito",
"NINE": "nove",
"TEN": "dez",
"TWELVE": "doze",
"FIFTEEN": "quinze",
"TWENTY": "vinte",
"THIRTY": "trinta"
}
}
}
@@ -4,6 +4,7 @@ import { ref, computed, watchEffect, onMounted } from 'vue';
import { useStore } from 'dashboard/composables/store';
import { useTrack } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import { useLocale } from 'shared/composables/useLocale';
import { useAppearanceHotKeys } from 'dashboard/composables/commands/useAppearanceHotKeys';
import { useInboxHotKeys } from 'dashboard/composables/commands/useInboxHotKeys';
import { useGoToCommandHotKeys } from 'dashboard/composables/commands/useGoToCommandHotKeys';
@@ -21,7 +22,8 @@ import {
import { emitter } from 'shared/helpers/mitt';
const store = useStore();
const { t } = useI18n();
const { t, tm } = useI18n();
const { resolvedLocale } = useLocale();
const ninjakeys = ref(null);
@@ -43,6 +45,8 @@ const SNOOZE_PARENT_IDS = [
];
const DYNAMIC_SNOOZE_PREFIX = 'dynamic_snooze_';
const CUSTOM_SNOOZE = wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME;
const dynamicSnoozeActions = ref([]);
const currentCommandRoot = ref(null);
@@ -77,8 +81,17 @@ const SNOOZE_SECTION_MAP = {
bulk_action_snooze_conversation: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS',
};
const snoozeTranslations = computed(() => {
const raw = tm('SNOOZE_PARSER');
if (!raw || typeof raw !== 'object') return {};
return JSON.parse(JSON.stringify(raw));
});
const buildDynamicSnoozeActions = (search, parentId) => {
const suggestions = generateSnoozeSuggestions(search);
const suggestions = generateSnoozeSuggestions(search, new Date(), {
translations: snoozeTranslations.value,
locale: resolvedLocale.value,
});
if (!suggestions.length) return [];
const busEvent = SNOOZE_EVENT_MAP[parentId];
@@ -98,6 +111,11 @@ const buildDynamicSnoozeActions = (search, parentId) => {
}));
};
const resetSnoozeState = () => {
currentCommandRoot.value = null;
dynamicSnoozeActions.value = [];
};
const patchNinjaKeysOpenClose = el => {
if (!el || typeof el.open !== 'function' || typeof el.close !== 'function') {
return;
@@ -114,8 +132,7 @@ const patchNinjaKeysOpenClose = el => {
};
el.close = (...args) => {
currentCommandRoot.value = null;
dynamicSnoozeActions.value = [];
resetSnoozeState();
return originalClose(...args);
};
};
@@ -126,23 +143,14 @@ const onSelected = item => {
action: { title = null, section = null, id = null, children = null } = {},
} = {},
} = item;
// Added this condition to prevent setting the selectedSnoozeType to null
// When we select the "custom snooze" (CMD bar will close and the custom snooze modal will open)
if (id === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
selectedSnoozeType.value = wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME;
} else {
selectedSnoozeType.value = null;
}
selectedSnoozeType.value = id === CUSTOM_SNOOZE ? id : null;
if (Array.isArray(children) && children.length) {
currentCommandRoot.value = id;
}
useTrack(GENERAL_EVENTS.COMMAND_BAR, {
section,
action: title,
});
useTrack(GENERAL_EVENTS.COMMAND_BAR, { section, action: title });
setCommandBarData();
};
@@ -176,15 +184,10 @@ const onCommandBarChange = item => {
};
const onClosed = () => {
// If the selectedSnoozeType is not "SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME (custom snooze)" then we set the context menu chat id to null
// Else we do nothing and its handled in the ChatList.vue hideCustomSnoozeModal() method
if (
selectedSnoozeType.value !== wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME
) {
if (selectedSnoozeType.value !== CUSTOM_SNOOZE) {
store.dispatch('setContextMenuChatId', null);
}
currentCommandRoot.value = null;
dynamicSnoozeActions.value = [];
resetSnoozeState();
};
watchEffect(() => {