Skip to content
This repository has been archived by the owner on Sep 27, 2024. It is now read-only.

Commit

Permalink
Merge branch 'main' into fix/discover-nav-update
Browse files Browse the repository at this point in the history
  • Loading branch information
anthony-liddle authored Oct 13, 2023
2 parents f70e075 + 3c06afa commit c6cd1d7
Show file tree
Hide file tree
Showing 28 changed files with 805 additions and 79 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ https-dev-config/localhost.key
Dockerfile
elk-translation-status.json
docs/translation-status.json
telemetry/generated
telemetry/README.md
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ public/emojis
*~
*swp
*swo

.venv/
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ Clone the repository and run on the root folder:

```
pnpm i
pnpm run build:glean
pnpm run dev
```

Expand All @@ -125,9 +126,18 @@ We recommend installing [ni](https://github.com/antfu/ni#ni), that will use the

```
ni
nr build:glean
nr dev
```

### Telemetry

*Note: This section is a work in progress.*

We are using [glean.js](https://github.com/mozilla/glean.js/) for analytics tracking. Build the necessary generated files by running the `build:glean` script before starting your server.

For more details on developing in Glean, see the [Glean Development Guide](./telemetry/README.md) (note it's currently very much a work in progress).

### Testing

Elk uses [Vitest](https://vitest.dev). You can run the test suite with:
Expand Down
1 change: 1 addition & 0 deletions components/nav/NavSide.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const useStarFavoriteIcon = usePreferences('useStarFavoriteIcon')
</div>
</template>
</NavSideItem>
<NavSideItem :text="$t('nav.invites')" to="/invites" icon="i-iconamoon:ticket" user-only :command="command" />
<NavSideItem :text="$t('nav.conversations')" to="/conversations" icon="i-ri:at-line" user-only :command="command" />
<NavSideItem :text="$t('nav.favourites')" to="/favourites" :icon="useStarFavoriteIcon ? 'i-ri:star-line' : 'i-ri:heart-3-line'" user-only :command="command" />
<NavSideItem :text="$t('nav.bookmarks')" to="/bookmarks" icon="i-ri:bookmark-line" user-only :command="command" />
Expand Down
66 changes: 66 additions & 0 deletions components/recommendation/RecommendationArticle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script setup lang="ts">
import type { Recommendation } from '../composables/recommendations'
const {
item,
} = defineProps<{
item: Recommendation
}>()
const target = ref<HTMLDivElement>()
const { isActive } = useIntersectionObserver(target, checkIntersection, { threshold: 0.5 })
function checkIntersection([{ isIntersecting }]: [{ isIntersecting: boolean }]): void {
if (isIntersecting && isActive.value) {
// fire analytics event here: item.id || item.title || item.url
isActive.value = false
}
}
const clipboard = useClipboard()
async function copyLink(url: string, event: Event): void {
event.preventDefault()
if (url)
await clipboard.copy(url)
}
</script>

<template>
<NuxtLink ref="target" :to="item.url" target="_blank" external p-y-16px p-x-8px flex border="b base">
<div class="content" w-full pr>
<h4 text-sm text-secondary>
{{ item.publisher }}
</h4>
<h3 text-lg line-height-tight font-500 m-y-4px>
{{ item.title }}
</h3>
<p>
{{ item.excerpt }}
</p>
</div>
<div class="media" relative overflow-hidden max-w-120px min-w-120px>
<img :src="item.image.sizes?.[0]?.url" rounded-lg overflow-hidden w-full ha>
<div m-y-4px flex flex-justify-end>
<button p-12px text-xl @click="copyLink(item.url, $event)">
<div i-ri:share-line />
</button>
</div>
</div>
</NuxtLink>
</template>

<style scoped>
a:hover h3 {
text-decoration: underline;
}
p {
line-height: 1.6em;
max-height: 4.8em;
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: anywhere;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
</style>
2 changes: 2 additions & 0 deletions components/settings/SettingsColorMode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const modes = [
<button
v-for="{ icon, label, mode } in modes"
:key="mode"
data-glean="settings.interface.colorMode"
:data-glean-value="`${mode}`"
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base ws-nowrap
:tabindex="colorMode.preference === mode ? 0 : -1"
:class="colorMode.preference === mode ? 'pointer-events-none' : 'filter-saturate-0'"
Expand Down
2 changes: 2 additions & 0 deletions components/settings/SettingsFontSize.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ function setFontSize(e: Event) {
:min="0"
:max="sizes.length - 1"
:step="1"
data-glean="settings.interface.fontSize"
:data-glean-value="`${sizes.indexOf(userSettings.fontSize)}`"
type="range"
focus:outline-none
appearance-none bg-transparent
Expand Down
49 changes: 49 additions & 0 deletions components/settings/SettingsInviteCodes.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script setup lang="ts">
const user = currentUser.value
interface InviteCode {
token: string
assigned?: string
assigned_at?: string
created_at: string
}
interface Invite {
tokens: InviteCode[]
}
const invite: Invite = await $fetch(`/api/${publicServer.value}/invites`, { headers: { authorization: `Bearer ${user?.token}` } })
const { text, copy, copied } = useClipboard()
const { t } = useI18n()
const noInvites = invite.tokens.length === 0
const heading = noInvites ? t('invites.no_codes.title') : t('invites.title')
const description = noInvites ? t('invites.no_codes.subtitle') : t('invites.subtitle')
</script>

<template>
<div p-x-5 sm:p-0>
<div m-b-5>
<h1 text-2xl p-y-2>
{{ heading }}
</h1>
<p>
{{ description }}
</p>
</div>
<main>
<div v-for="code in invite.tokens" :key="code.token" b-t-1px>
<div flex justify-between items-center b-b-1px p-4 m-b--1px>
<span>{{ code.token }}</span>
<div>
<button flex justify-between items-center @click="copy(code.token)">
<span v-if="copied && text === code.token" m-r-2 bg-primary text-base-light p-x-3 b-rd-10>{{ $t("invites.copied") }}</span>
<div i-ri:file-copy-line />
</button>
</div>
</div>
</div>
</main>
</div>
</template>
2 changes: 2 additions & 0 deletions components/settings/SettingsThemeColors.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ function updateTheme(theme: ThemeColors) {
}"
:class="currentTheme === key ? 'ring-2' : 'scale-90'"
:title="key"
data-glean="settings.interface.themeColor"
:data-glean-value="`${key}`"
w-8 h-8 rounded-full transition-all
ring="$local-ring-color offset-3 offset-$c-bg-base"
@click="updateTheme(theme)"
Expand Down
57 changes: 1 addition & 56 deletions components/timeline/TimelineDiscover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,68 +6,13 @@ const { locale: lang } = useI18n()
const locale = getLanguageForRecs(lang.value)
const recommendations: Recommendation[] = await $fetch(`/api/${publicServer.value}/recommendations?locale=${locale}`)
// Shorten a string to less than maxLen characters without truncating words.
function shorten(str: string, maxLen: number): string {
if (str.length <= maxLen)
return str
return `${str.slice(0, str.lastIndexOf(' ', maxLen))}...`
}
// This is temporary untill we are able to fetch from the new endpoint
function updateUTM(url: string): string {
return url.replace('pocket-newtab', 'mozilla')
}
const clipboard = useClipboard()
async function copyLink(url, event) {
event.preventDefault()
if (url)
await clipboard.copy(url)
}
</script>

<template>
<h1 text-2xl p-2>
Today’s Top Picks
</h1>
<main>
<template v-for="item in recommendations" :key="item.tileId">
<NuxtLink
:to="updateUTM(item.url)"
target="_blank"
external
p-y-16px
p-x-8px
flex
border="b base"
>
<div class="content" w-full pr>
<h4 text-sm text-secondary>
{{ item.publisher }}
</h4>
<h3 text-lg line-height-tight font-500 m-y-4px>
{{ shorten(item.title, 100) }}
</h3>
<p>
{{ shorten(item.excerpt, 140) }}
</p>
</div>
<div class="media" relative overflow-hidden max-w-120px min-w-120px>
<img :src="item.imageUrl" rounded-lg overflow-hidden w-full ha>
<div m-y-4px flex flex-justify-end>
<button p-12px text-xl @click="copyLink(updateUTM(item.url), $event)">
<div i-ri:share-line />
</button>
</div>
</div>
</NuxtLink>
</template>
<RecommendationArticle v-for="item in recommendations" :key="item.id" :item="item" />
</main>
</template>

<style scoped>
a:hover h3 {
text-decoration: underline;
}
</style>
2 changes: 1 addition & 1 deletion composables/masto/translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEd
&& supportedTranslationCodes.includes(to as any)
&& supportedTranslationCodes.includes(status.language as any)
&& !userSettings.value.disabledTranslationLanguages.includes(status.language)
const enabled = /*! !useRuntimeConfig().public.translateApi && */ shouldTranslate
const enabled = !!useRuntimeConfig().public.translateApi && shouldTranslate

async function toggle() {
if (!shouldTranslate)
Expand Down
3 changes: 3 additions & 0 deletions composables/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { mastodon } from 'masto'
import type { EffectScope, Ref } from 'vue'
import type { MaybeRefOrGetter, RemovableRef } from '@vueuse/core'
import type { ElkMasto } from './masto/masto'
import { mastodonAccountHandle, mastodonAccountId } from '~/telemetry/generated/identifiers'
import type { UserLogin } from '~/types'
import type { Overwrite } from '~/types/utils'
import {
Expand Down Expand Up @@ -166,6 +167,8 @@ export async function loginTo(masto: ElkMasto, user: Overwrite<UserLogin, { acco
})
}

mastodonAccountId.set(me.id)
mastodonAccountHandle.set(me.acct)
currentUserHandle.value = me.acct
}

Expand Down
11 changes: 11 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@
"footer_team": "The Elk Team",
"title": "Elk is in Preview!"
},
"invites": {
"copied": "Copied",
"label": "Invite",
"no_codes": {
"subtitle": "We're getting to work on your invite codes now. Come back in a few days and we’ll have codes ready for you to share with your friends.",
"title": "Check back soon"
},
"subtitle": "Copy a code below and paste into any message to invite a friend. Each code is one-time-use only.",
"title": "Invite a friend"
},
"language": {
"search": "Search"
},
Expand Down Expand Up @@ -289,6 +299,7 @@
"favourites": "Favorites",
"federated": "Federated",
"home": "Home",
"invites": "Invites",
"list": "List",
"lists": "Lists",
"local": "Local",
Expand Down
12 changes: 12 additions & 0 deletions modules/glean/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { addPlugin, createResolver, defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
meta: {
name: 'glean',
},
setup() {
const { resolve } = createResolver(import.meta.url)

addPlugin(resolve('./runtime/glean-plugin.client'))
},
})
66 changes: 66 additions & 0 deletions modules/glean/runtime/glean-plugin.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Glean from '@mozilla/glean/web'
import * as log from 'tauri-plugin-log-api'

import { linkClick, pageUrl, pageView, referrerUrl } from '../../../telemetry/generated/web'
import { userAgent } from '../../../telemetry/generated/identifiers'
import { engagement } from '../../../telemetry/generated/ui'
import { engagementDetails } from '../../../telemetry/engagementDetails'

export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('app:mounted', () => {
log.info('Glean: App mounted, start initing glean')

const GLEAN_APP_ID = 'mozilla-social-web'
const devMode = useAppConfig().env === ('dev' || 'canary' || 'preview')
const uploadEnabled = devMode // this will eventually be a setting that the user can toggle

Glean.initialize(GLEAN_APP_ID, uploadEnabled, {})
userAgent.set(navigator.userAgent)

// Debugging
if (devMode) {
Glean.setLogPings(true) // logs to the console
Glean.setDebugViewTag('moso-elk-dev') // logs to https://debug-ping-preview.firebaseapp.com
}

const eventListener = (event: MouseEvent) => {
if (event.type === 'click') {
handleButtonClick(event)
handleLinkClick(event)
}
}

function handleButtonClick(ev: MouseEvent) {
const eventTarget = ev?.target as Element
const closestButton = eventTarget.closest('button')

if (closestButton?.hasAttribute('href'))
linkClick.record({ target_url: closestButton.getAttribute('href') || '' })

const data = eventTarget?.getAttribute('data-glean') || ''
const value = eventTarget?.getAttribute('data-glean-value') || ''
if (eventTarget.hasAttribute('data-glean'))
engagement.record({ ui_identifier: data, engagement_value: value, ...engagementDetails[data] })
}

function handleLinkClick(ev: MouseEvent) {
const eventTarget = ev?.target as Element
const closestLink = eventTarget.closest('a')
if (closestLink)
linkClick.record({ target_url: closestLink.getAttribute('href') || '' })
}

window.addEventListener('click', eventListener)
})

nuxtApp.hook('page:finish', () => {
pageUrl.set(window.location.href)

if (document.referrer !== '')
referrerUrl.set(window.location.href)
else
referrerUrl.set('')

pageView.record()
})
})
Loading

0 comments on commit c6cd1d7

Please sign in to comment.