Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
"disabledHomeTimelineRedirectOption_messages": {
"message": "Messages"
},
"homeTimelineLimitLabel": {
"message": "Daily time limit"
},
"dontUseChirpFontLabel": {
"message": "Don't use the Chirp font"
},
Expand Down
14 changes: 14 additions & 0 deletions options.html
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,20 @@
</select>
</label>
</section>
<section class="select disabledHomeTimeline">
<label>
<span id="homeTimelineLimitLabel">Daily time limit</span>
<select name="homeTimelineLimit">
<option value="unlimited">Unlimited</option>
<option value="5">5 minutes</option>
<option value="15">15 minutes</option>
<option value="30">30 minutes</option>
</select>
</label>
<p id="homeTimelineLimitInfo" class="homeTimelineLimit">
<span id="homeTimelineLimitUsageLabel"></span>
</p>
</section>
</section>
<section class="select">
<label>
Expand Down
27 changes: 27 additions & 0 deletions options.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ const defaultConfig = {
defaultToLatestSearch: false,
disableHomeTimeline: false,
disabledHomeTimelineRedirect: 'notifications',
homeTimelineLimit: 'unlimited',
homeTimelineLimitData: {
date: new Date().toISOString().split('T')[0],
usedMs: 0,
lastActiveTime: null
},
disableTweetTextFormatting: false,
dontUseChirpFont: false,
dropdownMenuFontWeight: true,
Expand Down Expand Up @@ -334,6 +340,7 @@ let $mutedQuotesDetails = /** @type {HTMLDetailsElement} */ (document.querySele
let $mutedQuotesLabel = /** @type {HTMLElement} */ (document.querySelector('#mutedQuotesLabel'))
let $saveCustomCssButton = document.querySelector('button#saveCustomCss')
let $showBlueReplyFollowersCountLabel = /** @type {HTMLElement} */ (document.querySelector('#showBlueReplyFollowersCountLabel'))
let $homeTimelineLimitUsageLabel = /** @type {HTMLElement} */ (document.querySelector('#homeTimelineLimitUsageLabel'))
//#endregion

//#region Utility functions
Expand Down Expand Up @@ -498,6 +505,7 @@ function updateDisplay() {
$body.classList.toggle('chronological', optionsConfig.alwaysUseLatestTweets)
$body.classList.toggle('disabled', !optionsConfig.enabled)
$body.classList.toggle('disabledHomeTimeline', optionsConfig.disableHomeTimeline)
$body.classList.toggle('homeTimelineLimit', optionsConfig.disableHomeTimeline && optionsConfig.homeTimelineLimit !== 'unlimited')
$body.classList.toggle('fullWidthContent', optionsConfig.fullWidthContent)
$body.classList.toggle('hidingBookmarkButton', optionsConfig.hideBookmarkButton)
$body.classList.toggle('hidingExploreNav', optionsConfig.hideExploreNav)
Expand All @@ -517,6 +525,7 @@ function updateDisplay() {
)
updateHideQuotesFromDisplay()
updateMutedQuotesDisplay()
updateHomeTimelineLimitUsage()
}


Expand Down Expand Up @@ -580,6 +589,24 @@ function updateMutedQuotesDisplay() {
})
}

function updateHomeTimelineLimitUsage() {
if (!$homeTimelineLimitUsageLabel) {
return
}

if (optionsConfig.homeTimelineLimit === 'unlimited') {
$homeTimelineLimitUsageLabel.textContent = ''
return
}

let usedMinutes = Math.floor(optionsConfig.homeTimelineLimitData.usedMs / 60000)
let limitMinutes = parseInt(optionsConfig.homeTimelineLimit)
let remainingMinutes = Math.max(0, limitMinutes - usedMinutes)

$homeTimelineLimitUsageLabel.textContent =
`Used ${usedMinutes} of ${limitMinutes} minutes today (${remainingMinutes} remaining)`
}

function updateFormControls() {
Object.keys(optionsConfig)
.filter(prop => prop in $form.elements)
Expand Down
206 changes: 201 additions & 5 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ let lang
let dir
/** @type {boolean} */
let ltr
/** @type {number | null} */
let homeTimelineTrackingInterval = null

//#region Default config
/**
Expand All @@ -129,6 +131,12 @@ const config = {
defaultToLatestSearch: false,
disableHomeTimeline: false,
disabledHomeTimelineRedirect: 'notifications',
homeTimelineLimit: 'unlimited',
homeTimelineLimitData: {
date: new Date().toISOString().split('T')[0],
usedMs: 0,
lastActiveTime: null
},
disableTweetTextFormatting: false,
dontUseChirpFont: false,
dropdownMenuFontWeight: true,
Expand Down Expand Up @@ -3812,12 +3820,23 @@ async function addToggleListRetweetsMenuItem($switchMenuItem) {
}

/**
* Redirects away from the Home timeline if we're on it and it's been disabled.
* Redirects away from the Home timeline if we're on it and it's been disabled
* or the time limit has been exceeded.
* @returns {boolean} `true` if redirected as a result of this call
*/
function checkforDisabledHomeTimeline() {
if (config.disableHomeTimeline && location.pathname == PagePaths.HOME) {
log(`Home timeline disabled, redirecting to /${config.disabledHomeTimelineRedirect}`)
// Check if disabled with unlimited limit, or time limit exceeded
let shouldDisable = config.disableHomeTimeline && (
config.homeTimelineLimit === 'unlimited' ||
checkHomeTimelineLimit()
)

if (shouldDisable && location.pathname == PagePaths.HOME) {
log(`Home timeline ${config.homeTimelineLimit === 'unlimited' ? 'disabled' : 'time limit exceeded'}, redirecting to /${config.disabledHomeTimelineRedirect}`)

// Stop tracking before redirect
stopHomeTimelineTracking(true)

let primaryNavSelector = desktop ? Selectors.PRIMARY_NAV_DESKTOP : Selectors.PRIMARY_NAV_MOBILE
void (async () => {
let $navLink = await getElement(`${primaryNavSelector} a[href="/${config.disabledHomeTimelineRedirect}"]`, {
Expand All @@ -3829,8 +3848,148 @@ function checkforDisabledHomeTimeline() {
})()
return true
}
return false
}

//#region Home Timeline Time Limit

/**
* Check if a new day has started and reset the daily limit if so.
* @returns {boolean} True if reset occurred
*/
function checkAndResetDailyLimit() {
let today = new Date().toISOString().split('T')[0]
if (config.homeTimelineLimitData.date !== today) {
log('Daily home timeline limit reset - new day:', today)
config.homeTimelineLimitData = {
date: today,
usedMs: 0,
lastActiveTime: null
}
storeConfigChanges({homeTimelineLimitData: config.homeTimelineLimitData})
return true
}
return false
}

/**
* Get remaining time in milliseconds for home timeline viewing.
* @returns {number} Remaining milliseconds, or Infinity if unlimited
*/
function getRemainingHomeTimelineMs() {
if (config.homeTimelineLimit === 'unlimited') {
return Infinity
}
let limitMinutes = parseInt(config.homeTimelineLimit)
let limitMs = limitMinutes * 60 * 1000
let remaining = limitMs - config.homeTimelineLimitData.usedMs
return Math.max(0, remaining)
}

/**
* Check if the home timeline time limit has been exceeded.
* @returns {boolean} True if limit is exceeded
*/
function checkHomeTimelineLimit() {
if (config.homeTimelineLimit === 'unlimited') {
return false
}
return getRemainingHomeTimelineMs() <= 0
}

/**
* Initialize home timeline tracking on page load.
*/
function initializeHomeTimelineTracking() {
if (config.homeTimelineLimit === 'unlimited') {
return
}

checkAndResetDailyLimit()

if (isOnHomeTimelinePage() && !document.hidden) {
startHomeTimelineTracking()
}
}

/**
* Start tracking time spent on home timeline.
*/
function startHomeTimelineTracking() {
if (config.homeTimelineLimit === 'unlimited') {
return
}

if (homeTimelineTrackingInterval !== null) {
return // Already tracking
}

config.homeTimelineLimitData.lastActiveTime = Date.now()

// Update usage every 5 seconds
homeTimelineTrackingInterval = setInterval(() => {
updateHomeTimelineUsage()
}, 5000)

log('Started home timeline tracking')
}

/**
* Stop tracking time spent on home timeline.
* @param {boolean} saveUsage Whether to save the accumulated usage
*/
function stopHomeTimelineTracking(saveUsage = true) {
if (homeTimelineTrackingInterval === null) {
return // Not tracking
}

clearInterval(homeTimelineTrackingInterval)
homeTimelineTrackingInterval = null

if (saveUsage && config.homeTimelineLimitData.lastActiveTime !== null) {
let elapsed = Date.now() - config.homeTimelineLimitData.lastActiveTime
config.homeTimelineLimitData.usedMs += elapsed
config.homeTimelineLimitData.lastActiveTime = null
storeConfigChanges({homeTimelineLimitData: config.homeTimelineLimitData})
log('Stopped home timeline tracking, used:', Math.floor(config.homeTimelineLimitData.usedMs / 1000), 'seconds')
}

config.homeTimelineLimitData.lastActiveTime = null
}

/**
* Update accumulated home timeline usage and check for limit expiry.
*/
function updateHomeTimelineUsage() {
if (config.homeTimelineLimit === 'unlimited' || config.homeTimelineLimitData.lastActiveTime === null) {
return
}

// Check for midnight rollover
if (checkAndResetDailyLimit()) {
// Day changed, restart tracking with fresh limit
config.homeTimelineLimitData.lastActiveTime = Date.now()
return
}

// Add elapsed time to used time
let elapsed = Date.now() - config.homeTimelineLimitData.lastActiveTime
config.homeTimelineLimitData.usedMs += elapsed
config.homeTimelineLimitData.lastActiveTime = Date.now()

// Save to storage
storeConfigChanges({homeTimelineLimitData: config.homeTimelineLimitData})

// Check if limit exceeded
if (checkHomeTimelineLimit()) {
log('Home timeline time limit exceeded')
stopHomeTimelineTracking(false) // Already saved above
checkforDisabledHomeTimeline() // Trigger redirect
}
}

//#endregion

function checkReactNativeStylesheet() {
let $style = /** @type {HTMLStyleElement} */ (document.querySelector('style#react-native-stylesheet'))
if (!$style) {
Expand Down Expand Up @@ -4484,7 +4643,7 @@ const configureCss = (() => {
if (config.hideTimelineTweetBox) {
hideCssSelectors.push(`body.HomeTimeline ${Selectors.PRIMARY_COLUMN} .TweetBox`)
}
if (config.disableHomeTimeline) {
if (config.disableHomeTimeline && (config.homeTimelineLimit === 'unlimited' || checkHomeTimelineLimit())) {
hideCssSelectors.push(`${Selectors.PRIMARY_NAV_DESKTOP} a[href="/home"]`)
}
if (config.hideNotifications != 'ignore') {
Expand Down Expand Up @@ -4731,7 +4890,7 @@ const configureCss = (() => {
`)
}
}
if (config.disableHomeTimeline) {
if (config.disableHomeTimeline && (config.homeTimelineLimit === 'unlimited' || checkHomeTimelineLimit())) {
hideCssSelectors.push(`${Selectors.PRIMARY_NAV_MOBILE} a[href="/home"]`)
}
if (config.hideComposeTweet) {
Expand Down Expand Up @@ -6399,6 +6558,8 @@ function processCurrentPage() {
}
else {
removeMobileTimelineHeaderElements()
// Stop tracking when leaving home timeline
stopHomeTimelineTracking(true)
}

if (isOnProfilePage()) {
Expand Down Expand Up @@ -7061,6 +7222,11 @@ function tweakHomeTimelinePage() {
// Hook for styling when on the separated tweets tab
$body.classList.toggle('SeparatedTweets', isOnSeparatedTweetsTimeline())

// Start tracking time if time limit is enabled
if (!document.hidden) {
startHomeTimelineTracking()
}

if ($timelineTabs == null) {
warn('could not find Home timeline tabs')
return
Expand Down Expand Up @@ -7633,6 +7799,25 @@ async function main() {
}
}

// Set up home timeline time tracking
initializeHomeTimelineTracking()

// Listen for page visibility changes to pause/resume tracking
document.addEventListener('visibilitychange', () => {
if (config.homeTimelineLimit === 'unlimited') {
return
}

if (document.hidden) {
stopHomeTimelineTracking(true)
} else {
checkAndResetDailyLimit()
if (isOnHomeTimelinePage()) {
startHomeTimelineTracking()
}
}
})

// Repeatable configuration setup
configureSeparatedTweetsTimelineTitle()
configureCss()
Expand Down Expand Up @@ -7708,6 +7893,17 @@ function configChanged(changes) {
fontSize = desktop ? $html.style.fontSize : null
}

if ('homeTimelineLimit' in changes || 'disableHomeTimeline' in changes) {
if (config.homeTimelineLimit === 'unlimited') {
stopHomeTimelineTracking(true)
} else if (isOnHomeTimelinePage()) {
checkAndResetDailyLimit()
if (!document.hidden) {
startHomeTimelineTracking()
}
}
}

// Apply configuration changes
configureCss()
configureFont()
Expand Down
6 changes: 6 additions & 0 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export type Config = {
defaultToLatestSearch: boolean
disableHomeTimeline: boolean
disabledHomeTimelineRedirect: 'notifications' | 'messages'
homeTimelineLimit: 'unlimited' | '5' | '15' | '30'
homeTimelineLimitData: {
date: string
usedMs: number
lastActiveTime: number | null
}
disableTweetTextFormatting: boolean
dontUseChirpFont: boolean
dropdownMenuFontWeight: boolean
Expand Down