Question about improving the value of the favicon #3007
Zak Siddiqui opened 9 hours ago

When an issue or pull request has a new update (css .new-indicator), And the user currently does not have the tab open, consider painting a small circlular indicator on the bottom right of the favicon.

Then remove the indicator when the user opens the tab.

I know this can be done with extension icons, not sure about favicons.

  • Service Desk commented 9 hours ago

    Yes — a favicon can show a badge, but only on a tab that is already open.

    Favicon vs extension icon

    A browser-extension toolbar icon can be badged even when no OneDev tab exists. A favicon belongs to a loaded page, so if the issue or pull request is not open in any tab, there is nothing to paint. Closing the tab also drops the badge.

    What is possible

    For a background tab that already has the issue or pull request loaded, page JavaScript can:

    1. Draw the current site icon onto a canvas
    2. Paint a small circle in the bottom-right corner
    3. Replace <link rel="icon"> with the canvas image
    4. Restore the original icon when the tab becomes visible (document.hidden / visibilitychange)

    Chrome and Firefox generally update the tab icon this way. Safari (especially on iOS) has historically been unreliable with dynamically replaced favicons. A more portable extra cue is a title prefix such as a bullet, which several web apps use alongside or instead of a favicon badge.

    OneDev already serves the icon from the same origin (/~img/logo.png, or /logo.png when a custom branding logo is set), so canvas drawing is not blocked by CORS. Issue and pull request pages also already receive live WebSocket updates; new unread activity is marked with class new (the list column is .new-indicator). That is enough signal to badge a background tab.

    This is not implemented today. It would be a UI enhancement, not current behavior.

  • Zak Siddiqui commented 9 hours ago

    I am in favor of this approach.

    1. Draw the current site icon onto a canvas
    2. Paint a small circle in the bottom-right corner
    3. Replace <link rel="icon"> with the canvas image
    4. Restore the original icon when the tab becomes visible (document.hidden / visibilitychange)

    If we implement this, would it result in a conflict on Safari?

    For browsers that are not compatible with this feature, could we use a title prefix, such as a bullet? But not both, only one or the other.

    Don't assume. Ask high-level clarifying questions if needed.

    • Numbered questions (1, 2, 3, etc)
    • Lettered answer options (A, B, C, etc)
    • Include a markdown table that contains the following for each question:
      • Pros and Cons?
      • Your recommendation (including justification)?

    Once any questions have been answered, please create a new issue.

  • Service Desk commented 9 hours ago

    Safari does not conflict with the canvas badge in the sense of showing a broken or competing icon. Apple’s policy is that favicons should not be scripted, so Safari (including iOS / WebKit) typically keeps the original OneDev logo and ignores the <link rel="icon"> swap. Chrome and Firefox tabs are unaffected.

    The only practical risk is if we still rewrite the icon URL on Safari anyway. Safari caches favicons aggressively, so a data-URL or a failed canvas export can leave a blank or sticky icon. The safe rule is: do not touch <link rel="icon"> on Safari / WebKit; use a title prefix there instead.

    Yes — for browsers that cannot show the badge, a title prefix such as a bullet is the usual fallback. Using only one cue per browser is the right call: favicon badge where it works, title prefix everywhere else, never both on the same tab.

    OneDev already writes document.title during in-page history navigation (onedev.server.history.pushState / replaceState in base.js). A prefix would need to go through that path so AJAX navigation does not drop or double it.

    This remains an enhancement, not current behavior. After the questions below are answered, I will open a separate implementation issue.

    1. How should we decide which cue a browser gets?
    • A. Engine rule: Chromium and Firefox get the favicon badge; Safari / WebKit get a title prefix
    • B. Try the favicon badge first; fall back to a title prefix if we cannot confirm it applied
    • C. Title prefix in every browser (no favicon change)
    OptionProsConsRecommendation
    APredictable; never mutates the icon on Safari / iOS WebKitRelies on engine detection; a future Safari change would need a follow-upRecommended. Safari’s restriction is intentional and stable. An engine rule avoids blank/sticky icons without a fragile runtime check.
    BWould pick up a Safari that later allows scripted iconsWhether a favicon swap “took” is hard to detect; a failed swap can still leave a bad iconNot recommended.
    CSimplest; identical everywhereDrops the favicon badge on Chrome/FirefoxOnly if we want zero favicon risk.
    1. Which title prefix should unsupported browsers use?
    • A. A bullet: • Issue title
    • B. A filled circle: ● Issue title
    • C. A short word: (new) Issue title
    OptionProsConsRecommendation
    ACommon in web apps; compact; easy to stripEasy to miss next to a long titleRecommended. Matches the “small indicator” idea without clutter.
    BSlightly more visibleHeavier; can look like a status glyphFine if we want more contrast.
    CUnambiguousLonger; noisier in the tab stripUse only if a glyph is too subtle.
    1. Where should the indicator appear?
    • A. Issue and pull request detail pages only (the tab for that issue or PR)
    • B. Detail pages plus list pages when a row has .new-indicator
    • C. Any page that already shows unread activity with class new
    OptionProsConsRecommendation
    AMatches the original request; one tab maps to one issue/PRLists stay unchangedRecommended. Background-tab badges make the most sense on the entity you already opened.
    BLists also call you backA list tab would badge for any new row, which is a different product meaningDefer unless we want list-wide “something is new”.
    CBroadest coverageEasy to over-notify (dashboards, comment lists)Too wide for a first cut.
  • Zak Siddiqui commented 8 hours ago
    1. a
    2. b
    3. b. But only when there has been an updated new indicator. For example, within issues and pull requests, refreshing the page would remove the new indicator. In this scenario, we would always want the indicator to appear on the wave icon. On pages where there are lists, such as the issues page or the pull request page, refreshing the page shows a new indicator, which means something different. It means unread, and refreshing the page does not change that unread status. Therefore, we should only have a new indicator on the fav icon if there is a fresh new indicator in addition to what was already there whilst the page was already loaded. I hope that makes sense.

    Don't assume. Ask high-level clarifying questions if needed.

    • Numbered questions (1, 2, 3, etc)
    • Lettered answer options (A, B, C, etc)
    • Include a markdown table that contains the following for each question:
      • Pros and Cons?
      • Your recommendation (including justification)?
  • Service Desk commented 8 hours ago

    Thanks — that distinction is clear.

    Recorded answers

    1. Chromium and Firefox get the favicon badge; Safari / WebKit get a title prefix
    2. Title prefix is a filled circle: ● …
    3. Detail pages and list pages, with different triggers:
      • Issue/PR detail: any in-page .new activity can drive the cue (refresh clears those markers)
      • Issue/PR lists: ignore unread rows that were already there at load; only a fresh unread marker that appears while the page is already loaded should drive the cue

    A few decisions are still open before I file the implementation issue.

    1. After you look at a tab and the cue is cleared, when may it appear again?
    • A. Only if another fresh update arrives after you last focused the tab (re-baseline on focus)
    • B. As soon as you switch away again, if any .new markers are still on the page
    • C. Never again until the page is reloaded
    OptionProsConsRecommendation
    AMatches “remove when the tab is opened”; list pages will not keep nagging about unread you already glanced atA detail-page .new stripe stays in the DOM until refresh, so leaving the tab again will not re-badge unless a newer activity arrivesRecommended. Same rule for detail and lists: the cue means “something happened since you last looked at this tab.”
    BDetail pages would keep reminding you until refreshList pages would re-badge for unread that was already visibleConflicts with the list-page rule you described
    CSimplestA second comment while you are still on the same loaded page would be silent after the first lookToo weak for a background tab you keep open
    1. On list pages, what counts as a “fresh” unread marker?
    • A. A row that was not unread at last baseline becomes unread
    • B. A, plus further activity on a row that was already unread
    • C. Only a newly created issue or pull request showing up in the list (not updates to existing rows)
    OptionProsConsRecommendation
    AMatches “in addition to what was already there”; existing unread rows do not retriggerYou will not be called back for another comment on an issue that was already unread in that listRecommended. The extra marker is a new unread row, not more activity on an already-unread row.
    BMore complete for “something changed in this list”Noisy; the row was already flagged unreadEasy to over-notify
    CNarrowMisses the common live case: an existing listed item gets activity while the tab is in the backgroundToo narrow
    1. Which list pages should use that list rule?
    • A. All issue lists and all pull request lists (project and global / “my” lists)
    • B. A, plus code comment lists (they use the same unread column)
    • C. Only the project issues page and the project pull requests page
    OptionProsConsRecommendation
    ASame unread column and the same “refresh still shows unread” meaning everywhere those lists appearSlightly more surface than the two pages you namedRecommended. The meaning of the marker is the same on every issue/PR list.
    BConsistent for every .new-indicator tableCode comments were not part of the original requestDefer
    CSmallest scopeGlobal / “my” lists would behave differently from project listsInconsistent

    After these are answered I will open a separate implementation issue.

  • Zak Siddiqui commented 8 hours ago

    @servicedesk

    1. a
    2. a. Option A already makes option B redundant. If that is not the case, then please elaborate as to why you are suggesting option B as a feasible option
    3. b
  • Service Desk commented 8 hours ago

    @zaakiy Option A does not make option B redundant. They watch different signals.

    On list pages the unread stripe is a boolean class (tr.issue.new, tr.request.new, tr.comment.new). It is on when you have not visited since lastActivity, and it stays on until you open that item. Another comment on an issue that was already unread does not add a second stripe; the row was already new.

    Concrete case: the issues list is open in a background tab, issue 10 is already unread at load (or at last focus), and someone comments on issue 10 while that list is still loaded.

    OptionWhat happens
    ANo favicon/title cue. Issue 10 was already unread, so this is not a new unread row.
    BCue appears, because issue 10’s last activity changed even though it was already unread.

    A is “a row became unread that was not unread at baseline.” B is A plus “an already-unread row got further activity.” B is feasible, but it needs a second signal (last-activity time, or the row being live-updated), not the CSS class alone. That is why it was listed separately.

    Recorded so far

    1. Re-baseline on focus: the cue appears only if another fresh update arrives after you last focused the tab

    2. Pending confirmation below

    3. Issue lists, pull request lists, and code comment lists (same “fresh unread row” rule). Code comment detail pages stay out of scope unless you say otherwise.

    4. Given that distinction, which list-page rule do you want?

    • A. Only a row that was not unread at last baseline becoming unread (no cue for more activity on an already-unread row)
    • B. A, plus further activity on a row that was already unread
    OptionProsConsRecommendation
    AMatches “in addition to what was already there”; existing unread rows do not retriggerYou will not be called back for another comment on an item that was already unread in that listRecommended. Same meaning you described for refresh vs live: the extra marker is a new unread row.
    BCalls you back when an already-unread item gets more activityNoisier; that row was already flaggedUse only if a list tab should mean “anything in this list moved,” not “a new unread appeared.”

    After this is confirmed I will open a separate implementation issue.

  • Zak Siddiqui commented 8 hours ago
    1. A. Only a row that was not unread at last baseline becoming unread (no cue for more activity on an already-unread row)

    @servicedesk

    All decisions locked in. Please create a new issue. Please ensure that the new issue has all required context and motivation so that the AI agent or user does not need to look at this issue

  • Service Desk commented 8 hours ago

    Closing as answered; implementation tracked in issue #3009.

  • Service Desk commented 8 hours ago

    @zaakiy Created implementation issue #3009 with the agreed design:

    • Chromium and Firefox get the favicon badge; Safari / WebKit get a title prefix (never both)
    • Re-baseline on tab focus: the cue appears only if another fresh update arrives after you last looked at the tab
    • Issue and pull request detail pages: in-page new activity
    • Issue, pull request, and code comment lists: only a row that was not unread at last baseline becoming unread

    Closing this Question as answered.

  • Service Desk changed state to 'Closed' 8 hours ago
    Previous Value Current Value
    Open
    Closed
1/1
Type
Question
Priority
Normal
Assignee
Labels
No labels
Issue Votes (0)
Watchers (3)
Reference
OD-3007
Please wait...
Connection lost or session expired, reload to recover
Page is in error, reload to recover