diff --git a/apps/federatedfilesharing/src/components/PersonalSettings.vue b/apps/federatedfilesharing/src/components/PersonalSettings.vue
index 9b402a0ac31..7906d4c31d8 100644
--- a/apps/federatedfilesharing/src/components/PersonalSettings.vue
+++ b/apps/federatedfilesharing/src/components/PersonalSettings.vue
@@ -23,25 +23,31 @@
{{ t('federatedfilesharing', 'Share it so your friends can share files with you:') }}
-
+
{{ t('federatedfilesharing', 'Facebook') }}
+ :href="shareXUrl">
{{ t('federatedfilesharing', 'formerly Twitter') }}
-
+
{{ t('federatedfilesharing', 'Mastodon') }}
+
+ {{ t('federatedfilesharing', 'Bluesky') }}
+
+
+
+
@@ -101,6 +107,7 @@ export default {
reference: loadState('federatedfilesharing', 'reference'),
urlFacebookIcon: imagePath('core', 'facebook'),
urlMastodonIcon: imagePath('core', 'mastodon'),
+ urlBlueSkyIcon: imagePath('core', 'bluesky'),
urlXIcon: imagePath('core', 'x'),
}
},
@@ -130,6 +137,9 @@ export default {
shareFacebookUrl() {
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(this.reference)}`
},
+ shareBlueSkyUrl() {
+ return `https://bsky.app/intent/compose?text=${encodeURIComponent(this.messageWithURL)}`
+ },
logoPathAbsolute() {
return window.location.protocol + '//' + window.location.host + this.logoPath
},
@@ -176,7 +186,7 @@ export default {
.social-button {
margin-top: 0.5rem;
- button {
+ button, a {
display: inline-flex;
margin-inline-start: 0.5rem;
margin-top: 1rem;
diff --git a/apps/provisioning_api/lib/Controller/AUserDataOCSController.php b/apps/provisioning_api/lib/Controller/AUserDataOCSController.php
index 8c0763f4378..d321adf7c8f 100644
--- a/apps/provisioning_api/lib/Controller/AUserDataOCSController.php
+++ b/apps/provisioning_api/lib/Controller/AUserDataOCSController.php
@@ -164,6 +164,7 @@ abstract class AUserDataOCSController extends OCSController {
IAccountManager::PROPERTY_ADDRESS,
IAccountManager::PROPERTY_WEBSITE,
IAccountManager::PROPERTY_TWITTER,
+ IAccountManager::PROPERTY_BLUESKY,
IAccountManager::PROPERTY_FEDIVERSE,
IAccountManager::PROPERTY_ORGANISATION,
IAccountManager::PROPERTY_ROLE,
diff --git a/apps/provisioning_api/lib/Controller/UsersController.php b/apps/provisioning_api/lib/Controller/UsersController.php
index 1d1d0e8d2f9..513a27c7df8 100644
--- a/apps/provisioning_api/lib/Controller/UsersController.php
+++ b/apps/provisioning_api/lib/Controller/UsersController.php
@@ -788,6 +788,7 @@ class UsersController extends AUserDataOCSController {
$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
$permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
$permittedFields[] = IAccountManager::PROPERTY_TWITTER;
+ $permittedFields[] = IAccountManager::PROPERTY_BLUESKY;
$permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
$permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
$permittedFields[] = IAccountManager::PROPERTY_ROLE;
@@ -974,6 +975,7 @@ class UsersController extends AUserDataOCSController {
$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
$permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
$permittedFields[] = IAccountManager::PROPERTY_TWITTER;
+ $permittedFields[] = IAccountManager::PROPERTY_BLUESKY;
$permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
$permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
$permittedFields[] = IAccountManager::PROPERTY_ROLE;
@@ -987,6 +989,7 @@ class UsersController extends AUserDataOCSController {
$permittedFields[] = IAccountManager::PROPERTY_ADDRESS . self::SCOPE_SUFFIX;
$permittedFields[] = IAccountManager::PROPERTY_WEBSITE . self::SCOPE_SUFFIX;
$permittedFields[] = IAccountManager::PROPERTY_TWITTER . self::SCOPE_SUFFIX;
+ $permittedFields[] = IAccountManager::PROPERTY_BLUESKY . self::SCOPE_SUFFIX;
$permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE . self::SCOPE_SUFFIX;
$permittedFields[] = IAccountManager::PROPERTY_ORGANISATION . self::SCOPE_SUFFIX;
$permittedFields[] = IAccountManager::PROPERTY_ROLE . self::SCOPE_SUFFIX;
@@ -1030,6 +1033,7 @@ class UsersController extends AUserDataOCSController {
$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
$permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
$permittedFields[] = IAccountManager::PROPERTY_TWITTER;
+ $permittedFields[] = IAccountManager::PROPERTY_BLUESKY;
$permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
$permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
$permittedFields[] = IAccountManager::PROPERTY_ROLE;
@@ -1177,6 +1181,7 @@ class UsersController extends AUserDataOCSController {
case IAccountManager::PROPERTY_ADDRESS:
case IAccountManager::PROPERTY_WEBSITE:
case IAccountManager::PROPERTY_TWITTER:
+ case IAccountManager::PROPERTY_BLUESKY:
case IAccountManager::PROPERTY_FEDIVERSE:
case IAccountManager::PROPERTY_ORGANISATION:
case IAccountManager::PROPERTY_ROLE:
@@ -1224,6 +1229,7 @@ class UsersController extends AUserDataOCSController {
case IAccountManager::PROPERTY_ADDRESS . self::SCOPE_SUFFIX:
case IAccountManager::PROPERTY_WEBSITE . self::SCOPE_SUFFIX:
case IAccountManager::PROPERTY_TWITTER . self::SCOPE_SUFFIX:
+ case IAccountManager::PROPERTY_BLUESKY . self::SCOPE_SUFFIX:
case IAccountManager::PROPERTY_FEDIVERSE . self::SCOPE_SUFFIX:
case IAccountManager::PROPERTY_ORGANISATION . self::SCOPE_SUFFIX:
case IAccountManager::PROPERTY_ROLE . self::SCOPE_SUFFIX:
diff --git a/apps/provisioning_api/lib/ResponseDefinitions.php b/apps/provisioning_api/lib/ResponseDefinitions.php
index d41a78746b5..62ae4ca577b 100644
--- a/apps/provisioning_api/lib/ResponseDefinitions.php
+++ b/apps/provisioning_api/lib/ResponseDefinitions.php
@@ -67,6 +67,8 @@ namespace OCA\Provisioning_API;
* subadmin: list,
* twitter: string,
* twitterScope?: Provisioning_APIUserDetailsScope,
+ * bluesky: string,
+ * blueskyScope?: Provisioning_APIUserDetailsScope,
* website: string,
* websiteScope?: Provisioning_APIUserDetailsScope,
* }
diff --git a/apps/provisioning_api/tests/Controller/UsersControllerTest.php b/apps/provisioning_api/tests/Controller/UsersControllerTest.php
index e838cd16633..0c0a0ae3d74 100644
--- a/apps/provisioning_api/tests/Controller/UsersControllerTest.php
+++ b/apps/provisioning_api/tests/Controller/UsersControllerTest.php
@@ -1143,6 +1143,7 @@ class UsersControllerTest extends TestCase {
IAccountManager::PROPERTY_ADDRESS => ['value' => 'address'],
IAccountManager::PROPERTY_PHONE => ['value' => 'phone'],
IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'],
+ IAccountManager::PROPERTY_BLUESKY => ['value' => 'bluesky'],
IAccountManager::PROPERTY_FEDIVERSE => ['value' => 'fediverse'],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'],
IAccountManager::PROPERTY_ORGANISATION => ['value' => 'organisation'],
@@ -1219,6 +1220,7 @@ class UsersControllerTest extends TestCase {
'address' => 'address',
'website' => 'website',
'twitter' => 'twitter',
+ 'bluesky' => 'bluesky',
'fediverse' => 'fediverse',
'groups' => ['group0', 'group1', 'group2'],
'language' => 'de',
@@ -1332,6 +1334,7 @@ class UsersControllerTest extends TestCase {
IAccountManager::PROPERTY_ADDRESS => ['value' => 'address'],
IAccountManager::PROPERTY_PHONE => ['value' => 'phone'],
IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'],
+ IAccountManager::PROPERTY_BLUESKY => ['value' => 'bluesky'],
IAccountManager::PROPERTY_FEDIVERSE => ['value' => 'fediverse'],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'],
IAccountManager::PROPERTY_ORGANISATION => ['value' => 'organisation'],
@@ -1364,6 +1367,7 @@ class UsersControllerTest extends TestCase {
'address' => 'address',
'website' => 'website',
'twitter' => 'twitter',
+ 'bluesky' => 'bluesky',
'fediverse' => 'fediverse',
'groups' => [],
'language' => 'da',
@@ -1516,6 +1520,7 @@ class UsersControllerTest extends TestCase {
IAccountManager::PROPERTY_ADDRESS => ['value' => 'address'],
IAccountManager::PROPERTY_PHONE => ['value' => 'phone'],
IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'],
+ IAccountManager::PROPERTY_BLUESKY => ['value' => 'bluesky'],
IAccountManager::PROPERTY_FEDIVERSE => ['value' => 'fediverse'],
IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'],
IAccountManager::PROPERTY_ORGANISATION => ['value' => 'organisation'],
@@ -1547,6 +1552,7 @@ class UsersControllerTest extends TestCase {
'address' => 'address',
'website' => 'website',
'twitter' => 'twitter',
+ 'bluesky' => 'bluesky',
'fediverse' => 'fediverse',
'groups' => [],
'language' => 'ru',
@@ -1894,6 +1900,7 @@ class UsersControllerTest extends TestCase {
public static function selfEditChangePropertyProvider(): array {
return [
[IAccountManager::PROPERTY_TWITTER, '@oldtwitter', '@newtwitter'],
+ [IAccountManager::PROPERTY_BLUESKY, 'old.bluesky', 'new.bluesky'],
[IAccountManager::PROPERTY_FEDIVERSE, '@oldFediverse@floss.social', '@newFediverse@floss.social'],
[IAccountManager::PROPERTY_PHONE, '1234', '12345'],
[IAccountManager::PROPERTY_ADDRESS, 'Something street 2', 'Another street 3'],
@@ -1970,6 +1977,7 @@ class UsersControllerTest extends TestCase {
[IAccountManager::PROPERTY_DISPLAYNAME, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
[IAccountManager::PROPERTY_EMAIL, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
[IAccountManager::PROPERTY_TWITTER, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
+ [IAccountManager::PROPERTY_BLUESKY, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
[IAccountManager::PROPERTY_FEDIVERSE, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
[IAccountManager::PROPERTY_PHONE, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
[IAccountManager::PROPERTY_ADDRESS, IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_FEDERATED],
@@ -3856,6 +3864,7 @@ class UsersControllerTest extends TestCase {
'address' => 'address',
'website' => 'website',
'twitter' => 'twitter',
+ 'bluesky' => 'bluesky',
'fediverse' => 'fediverse',
'organisation' => 'organisation',
'role' => 'role',
@@ -3877,6 +3886,7 @@ class UsersControllerTest extends TestCase {
'address' => 'address',
'website' => 'website',
'twitter' => 'twitter',
+ 'bluesky' => 'bluesky',
'fediverse' => 'fediverse',
'organisation' => 'organisation',
'role' => 'role',
@@ -3944,6 +3954,7 @@ class UsersControllerTest extends TestCase {
'address' => 'address',
'website' => 'website',
'twitter' => 'twitter',
+ 'bluesky' => 'bluesky',
'fediverse' => 'fediverse',
'displayname' => 'Demo User',
'display-name' => 'Demo User',
@@ -4286,6 +4297,7 @@ class UsersControllerTest extends TestCase {
IAccountManager::PROPERTY_ADDRESS,
IAccountManager::PROPERTY_WEBSITE,
IAccountManager::PROPERTY_TWITTER,
+ IAccountManager::PROPERTY_BLUESKY,
IAccountManager::PROPERTY_FEDIVERSE,
IAccountManager::PROPERTY_ORGANISATION,
IAccountManager::PROPERTY_ROLE,
@@ -4301,6 +4313,7 @@ class UsersControllerTest extends TestCase {
IAccountManager::PROPERTY_ADDRESS,
IAccountManager::PROPERTY_WEBSITE,
IAccountManager::PROPERTY_TWITTER,
+ IAccountManager::PROPERTY_BLUESKY,
IAccountManager::PROPERTY_FEDIVERSE,
IAccountManager::PROPERTY_ORGANISATION,
IAccountManager::PROPERTY_ROLE,
@@ -4317,6 +4330,7 @@ class UsersControllerTest extends TestCase {
IAccountManager::PROPERTY_ADDRESS,
IAccountManager::PROPERTY_WEBSITE,
IAccountManager::PROPERTY_TWITTER,
+ IAccountManager::PROPERTY_BLUESKY,
IAccountManager::PROPERTY_FEDIVERSE,
IAccountManager::PROPERTY_ORGANISATION,
IAccountManager::PROPERTY_ROLE,
@@ -4331,6 +4345,7 @@ class UsersControllerTest extends TestCase {
IAccountManager::PROPERTY_ADDRESS,
IAccountManager::PROPERTY_WEBSITE,
IAccountManager::PROPERTY_TWITTER,
+ IAccountManager::PROPERTY_BLUESKY,
IAccountManager::PROPERTY_FEDIVERSE,
IAccountManager::PROPERTY_ORGANISATION,
IAccountManager::PROPERTY_ROLE,
@@ -4346,6 +4361,7 @@ class UsersControllerTest extends TestCase {
IAccountManager::PROPERTY_ADDRESS,
IAccountManager::PROPERTY_WEBSITE,
IAccountManager::PROPERTY_TWITTER,
+ IAccountManager::PROPERTY_BLUESKY,
IAccountManager::PROPERTY_FEDIVERSE,
IAccountManager::PROPERTY_ORGANISATION,
IAccountManager::PROPERTY_ROLE,
@@ -4360,6 +4376,7 @@ class UsersControllerTest extends TestCase {
IAccountManager::PROPERTY_ADDRESS,
IAccountManager::PROPERTY_WEBSITE,
IAccountManager::PROPERTY_TWITTER,
+ IAccountManager::PROPERTY_BLUESKY,
IAccountManager::PROPERTY_FEDIVERSE,
IAccountManager::PROPERTY_ORGANISATION,
IAccountManager::PROPERTY_ROLE,
@@ -4375,6 +4392,7 @@ class UsersControllerTest extends TestCase {
IAccountManager::PROPERTY_ADDRESS,
IAccountManager::PROPERTY_WEBSITE,
IAccountManager::PROPERTY_TWITTER,
+ IAccountManager::PROPERTY_BLUESKY,
IAccountManager::PROPERTY_FEDIVERSE,
IAccountManager::PROPERTY_ORGANISATION,
IAccountManager::PROPERTY_ROLE,
@@ -4389,6 +4407,7 @@ class UsersControllerTest extends TestCase {
IAccountManager::PROPERTY_ADDRESS,
IAccountManager::PROPERTY_WEBSITE,
IAccountManager::PROPERTY_TWITTER,
+ IAccountManager::PROPERTY_BLUESKY,
IAccountManager::PROPERTY_FEDIVERSE,
IAccountManager::PROPERTY_ORGANISATION,
IAccountManager::PROPERTY_ROLE,
diff --git a/apps/settings/lib/Controller/UsersController.php b/apps/settings/lib/Controller/UsersController.php
index 6cd596d6cc8..8efd3eeb8ca 100644
--- a/apps/settings/lib/Controller/UsersController.php
+++ b/apps/settings/lib/Controller/UsersController.php
@@ -319,6 +319,8 @@ class UsersController extends Controller {
* @param string|null $addressScope
* @param string|null $twitter
* @param string|null $twitterScope
+ * @param string|null $bluesky
+ * @param string|null $blueskyScope
* @param string|null $fediverse
* @param string|null $fediverseScope
* @param string|null $birthdate
@@ -342,6 +344,8 @@ class UsersController extends Controller {
?string $addressScope = null,
?string $twitter = null,
?string $twitterScope = null,
+ ?string $bluesky = null,
+ ?string $blueskyScope = null,
?string $fediverse = null,
?string $fediverseScope = null,
?string $birthdate = null,
@@ -386,6 +390,7 @@ class UsersController extends Controller {
IAccountManager::PROPERTY_ADDRESS => ['value' => $address, 'scope' => $addressScope],
IAccountManager::PROPERTY_PHONE => ['value' => $phone, 'scope' => $phoneScope],
IAccountManager::PROPERTY_TWITTER => ['value' => $twitter, 'scope' => $twitterScope],
+ IAccountManager::PROPERTY_BLUESKY => ['value' => $bluesky, 'scope' => $blueskyScope],
IAccountManager::PROPERTY_FEDIVERSE => ['value' => $fediverse, 'scope' => $fediverseScope],
IAccountManager::PROPERTY_BIRTHDATE => ['value' => $birthdate, 'scope' => $birthdateScope],
IAccountManager::PROPERTY_PRONOUNS => ['value' => $pronouns, 'scope' => $pronounsScope],
@@ -428,6 +433,8 @@ class UsersController extends Controller {
'addressScope' => $userAccount->getProperty(IAccountManager::PROPERTY_ADDRESS)->getScope(),
'twitter' => $userAccount->getProperty(IAccountManager::PROPERTY_TWITTER)->getValue(),
'twitterScope' => $userAccount->getProperty(IAccountManager::PROPERTY_TWITTER)->getScope(),
+ 'bluesky' => $userAccount->getProperty(IAccountManager::PROPERTY_BLUESKY)->getValue(),
+ 'blueskyScope' => $userAccount->getProperty(IAccountManager::PROPERTY_BLUESKY)->getScope(),
'fediverse' => $userAccount->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getValue(),
'fediverseScope' => $userAccount->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getScope(),
'birthdate' => $userAccount->getProperty(IAccountManager::PROPERTY_BIRTHDATE)->getValue(),
diff --git a/apps/settings/lib/Settings/Personal/PersonalInfo.php b/apps/settings/lib/Settings/Personal/PersonalInfo.php
index 84b379f2e87..9a12b18bb5e 100644
--- a/apps/settings/lib/Settings/Personal/PersonalInfo.php
+++ b/apps/settings/lib/Settings/Personal/PersonalInfo.php
@@ -98,6 +98,7 @@ class PersonalInfo implements ISettings {
'location' => $this->getProperty($account, IAccountManager::PROPERTY_ADDRESS),
'website' => $this->getProperty($account, IAccountManager::PROPERTY_WEBSITE),
'twitter' => $this->getProperty($account, IAccountManager::PROPERTY_TWITTER),
+ 'bluesky' => $this->getProperty($account, IAccountManager::PROPERTY_BLUESKY),
'fediverse' => $this->getProperty($account, IAccountManager::PROPERTY_FEDIVERSE),
'languageMap' => $this->getLanguageMap($user),
'localeMap' => $this->getLocaleMap($user),
diff --git a/apps/settings/src/components/PersonalInfo/BlueskySection.vue b/apps/settings/src/components/PersonalInfo/BlueskySection.vue
new file mode 100644
index 00000000000..1e804e2fb46
--- /dev/null
+++ b/apps/settings/src/components/PersonalInfo/BlueskySection.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
diff --git a/apps/settings/src/constants/AccountPropertyConstants.ts b/apps/settings/src/constants/AccountPropertyConstants.ts
index 455c210976f..575a2744cc6 100644
--- a/apps/settings/src/constants/AccountPropertyConstants.ts
+++ b/apps/settings/src/constants/AccountPropertyConstants.ts
@@ -28,6 +28,7 @@ export const ACCOUNT_PROPERTY_ENUM = Object.freeze({
PRONOUNS: 'pronouns',
ROLE: 'role',
TWITTER: 'twitter',
+ BLUESKY: 'bluesky',
WEBSITE: 'website',
})
@@ -48,6 +49,7 @@ export const ACCOUNT_PROPERTY_READABLE_ENUM = Object.freeze({
PRONOUNS: t('settings', 'Pronouns'),
ROLE: t('settings', 'Role'),
TWITTER: t('settings', 'X (formerly Twitter)'),
+ BLUESKY: t('settings', 'Bluesky'),
WEBSITE: t('settings', 'Website'),
})
@@ -64,6 +66,7 @@ export const NAME_READABLE_ENUM = Object.freeze({
[ACCOUNT_PROPERTY_ENUM.PROFILE_ENABLED]: ACCOUNT_PROPERTY_READABLE_ENUM.PROFILE_ENABLED,
[ACCOUNT_PROPERTY_ENUM.ROLE]: ACCOUNT_PROPERTY_READABLE_ENUM.ROLE,
[ACCOUNT_PROPERTY_ENUM.TWITTER]: ACCOUNT_PROPERTY_READABLE_ENUM.TWITTER,
+ [ACCOUNT_PROPERTY_ENUM.BLUESKY]: ACCOUNT_PROPERTY_READABLE_ENUM.BLUESKY,
[ACCOUNT_PROPERTY_ENUM.FEDIVERSE]: ACCOUNT_PROPERTY_READABLE_ENUM.FEDIVERSE,
[ACCOUNT_PROPERTY_ENUM.WEBSITE]: ACCOUNT_PROPERTY_READABLE_ENUM.WEBSITE,
[ACCOUNT_PROPERTY_ENUM.BIRTHDATE]: ACCOUNT_PROPERTY_READABLE_ENUM.BIRTHDATE,
@@ -89,6 +92,7 @@ export const PROPERTY_READABLE_KEYS_ENUM = Object.freeze({
[ACCOUNT_PROPERTY_READABLE_ENUM.PROFILE_ENABLED]: ACCOUNT_PROPERTY_ENUM.PROFILE_ENABLED,
[ACCOUNT_PROPERTY_READABLE_ENUM.ROLE]: ACCOUNT_PROPERTY_ENUM.ROLE,
[ACCOUNT_PROPERTY_READABLE_ENUM.TWITTER]: ACCOUNT_PROPERTY_ENUM.TWITTER,
+ [ACCOUNT_PROPERTY_READABLE_ENUM.BLUESKY]: ACCOUNT_PROPERTY_ENUM.BLUESKY,
[ACCOUNT_PROPERTY_READABLE_ENUM.FEDIVERSE]: ACCOUNT_PROPERTY_ENUM.FEDIVERSE,
[ACCOUNT_PROPERTY_READABLE_ENUM.WEBSITE]: ACCOUNT_PROPERTY_ENUM.WEBSITE,
[ACCOUNT_PROPERTY_READABLE_ENUM.BIRTHDATE]: ACCOUNT_PROPERTY_ENUM.BIRTHDATE,
@@ -135,6 +139,7 @@ export const PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM = Object.freeze({
[ACCOUNT_PROPERTY_READABLE_ENUM.PROFILE_ENABLED]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
[ACCOUNT_PROPERTY_READABLE_ENUM.ROLE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
[ACCOUNT_PROPERTY_READABLE_ENUM.TWITTER]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
+ [ACCOUNT_PROPERTY_READABLE_ENUM.BLUESKY]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
[ACCOUNT_PROPERTY_READABLE_ENUM.FEDIVERSE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
[ACCOUNT_PROPERTY_READABLE_ENUM.WEBSITE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
[ACCOUNT_PROPERTY_READABLE_ENUM.BIRTHDATE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
diff --git a/apps/settings/src/main-personal-info.js b/apps/settings/src/main-personal-info.js
index c28f14ee477..5ccfc9848c0 100644
--- a/apps/settings/src/main-personal-info.js
+++ b/apps/settings/src/main-personal-info.js
@@ -27,6 +27,7 @@ import ProfileVisibilitySection from './components/PersonalInfo/ProfileVisibilit
import PronounsSection from './components/PersonalInfo/PronounsSection.vue'
import RoleSection from './components/PersonalInfo/RoleSection.vue'
import TwitterSection from './components/PersonalInfo/TwitterSection.vue'
+import BlueskySection from './components/PersonalInfo/BlueskySection.vue'
import WebsiteSection from './components/PersonalInfo/WebsiteSection.vue'
__webpack_nonce__ = getCSPNonce()
@@ -52,6 +53,7 @@ const LocationView = Vue.extend(LocationSection)
const PhoneView = Vue.extend(PhoneSection)
const PronounsView = Vue.extend(PronounsSection)
const TwitterView = Vue.extend(TwitterSection)
+const BlueskyView = Vue.extend(BlueskySection)
const WebsiteView = Vue.extend(WebsiteSection)
new AvatarView().$mount('#vue-avatar-section')
@@ -62,6 +64,7 @@ new PhoneView().$mount('#vue-phone-section')
new LocationView().$mount('#vue-location-section')
new WebsiteView().$mount('#vue-website-section')
new TwitterView().$mount('#vue-twitter-section')
+new BlueskyView().$mount('#vue-bluesky-section')
new FediverseView().$mount('#vue-fediverse-section')
new LanguageView().$mount('#vue-language-section')
new LocaleView().$mount('#vue-locale-section')
diff --git a/apps/settings/templates/settings/personal/personal.info.php b/apps/settings/templates/settings/personal/personal.info.php
index e622663fba7..38c449a337f 100644
--- a/apps/settings/templates/settings/personal/personal.info.php
+++ b/apps/settings/templates/settings/personal/personal.info.php
@@ -73,6 +73,9 @@ script('settings', [
+
diff --git a/apps/settings/tests/Controller/UsersControllerTest.php b/apps/settings/tests/Controller/UsersControllerTest.php
index 0468ce6814c..1012557bfc4 100644
--- a/apps/settings/tests/Controller/UsersControllerTest.php
+++ b/apps/settings/tests/Controller/UsersControllerTest.php
@@ -202,6 +202,11 @@ class UsersControllerTest extends \Test\TestCase {
'Default twitter',
IAccountManager::SCOPE_LOCAL,
),
+ IAccountManager::PROPERTY_BLUESKY => $this->buildPropertyMock(
+ IAccountManager::PROPERTY_BLUESKY,
+ 'Default bluesky',
+ IAccountManager::SCOPE_LOCAL,
+ ),
IAccountManager::PROPERTY_FEDIVERSE => $this->buildPropertyMock(
IAccountManager::PROPERTY_FEDIVERSE,
'Default fediverse',
@@ -435,6 +440,8 @@ class UsersControllerTest extends \Test\TestCase {
$addressScope = IAccountManager::SCOPE_PUBLISHED;
$twitter = '@nextclouders';
$twitterScope = IAccountManager::SCOPE_PUBLISHED;
+ $bluesky = 'nextclouders.net';
+ $blueskyScope = IAccountManager::SCOPE_PUBLISHED;
$fediverse = '@nextclouders@floss.social';
$fediverseScope = IAccountManager::SCOPE_PUBLISHED;
$birthdate = '2020-01-01';
@@ -458,6 +465,8 @@ class UsersControllerTest extends \Test\TestCase {
$expectedProperties[IAccountManager::PROPERTY_ADDRESS]['scope'] = $addressScope;
$expectedProperties[IAccountManager::PROPERTY_TWITTER]['value'] = $twitter;
$expectedProperties[IAccountManager::PROPERTY_TWITTER]['scope'] = $twitterScope;
+ $expectedProperties[IAccountManager::PROPERTY_BLUESKY]['value'] = $bluesky;
+ $expectedProperties[IAccountManager::PROPERTY_BLUESKY]['scope'] = $blueskyScope;
$expectedProperties[IAccountManager::PROPERTY_FEDIVERSE]['value'] = $fediverse;
$expectedProperties[IAccountManager::PROPERTY_FEDIVERSE]['scope'] = $fediverseScope;
$expectedProperties[IAccountManager::PROPERTY_BIRTHDATE]['value'] = $birthdate;
@@ -486,6 +495,8 @@ class UsersControllerTest extends \Test\TestCase {
$addressScope,
$twitter,
$twitterScope,
+ $bluesky,
+ $blueskyScope,
$fediverse,
$fediverseScope,
$birthdate,
@@ -524,6 +535,8 @@ class UsersControllerTest extends \Test\TestCase {
$addressScope = ($property === 'addressScope') ? $propertyValue : null;
$twitter = ($property === 'twitter') ? $propertyValue : null;
$twitterScope = ($property === 'twitterScope') ? $propertyValue : null;
+ $bluesky = ($property === 'bluesky') ? $propertyValue : null;
+ $blueskyScope = ($property === 'blueskyScope') ? $propertyValue : null;
$fediverse = ($property === 'fediverse') ? $propertyValue : null;
$fediverseScope = ($property === 'fediverseScope') ? $propertyValue : null;
$birthdate = ($property === 'birthdate') ? $propertyValue : null;
@@ -562,6 +575,10 @@ class UsersControllerTest extends \Test\TestCase {
case 'twitterScope':
$propertyId = IAccountManager::PROPERTY_TWITTER;
break;
+ case 'bluesky':
+ case 'blueskyScope':
+ $propertyId = IAccountManager::PROPERTY_BLUESKY;
+ break;
case 'fediverse':
case 'fediverseScope':
$propertyId = IAccountManager::PROPERTY_FEDIVERSE;
@@ -604,6 +621,8 @@ class UsersControllerTest extends \Test\TestCase {
$addressScope,
$twitter,
$twitterScope,
+ $bluesky,
+ $blueskyScope,
$fediverse,
$fediverseScope,
$birthdate,
@@ -628,6 +647,8 @@ class UsersControllerTest extends \Test\TestCase {
['addressScope', IAccountManager::SCOPE_PUBLISHED],
['twitter', '@nextclouders'],
['twitterScope', IAccountManager::SCOPE_PUBLISHED],
+ ['bluesky', 'nextclouders.net'],
+ ['blueskyScope', IAccountManager::SCOPE_PUBLISHED],
['fediverse', '@nextclouders@floss.social'],
['fediverseScope', IAccountManager::SCOPE_PUBLISHED],
['birthdate', '2020-01-01'],
diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature
index 6c35e5a68f1..8fcfb076497 100644
--- a/build/integration/features/provisioning-v1.feature
+++ b/build/integration/features/provisioning-v1.feature
@@ -72,7 +72,8 @@ Feature: provisioning
| phone |
| address |
| website |
- | twitter |
+ | twitter |
+ | bluesky |
| fediverse |
| organisation |
| role |
@@ -89,6 +90,7 @@ Feature: provisioning
| address |
| website |
| twitter |
+ | bluesky |
| fediverse |
| organisation |
| role |
@@ -104,6 +106,7 @@ Feature: provisioning
| address |
| website |
| twitter |
+ | bluesky |
| fediverse |
| organisation |
| role |
@@ -158,6 +161,9 @@ Feature: provisioning
And sending "PUT" to "/cloud/users/brand-new-user" with
| key | twitter |
| value | Nextcloud |
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | bluesky |
+ | value | nextcloud.bsky.social |
And the OCS status code should be "100"
And the HTTP status code should be "200"
Then user "brand-new-user" has
@@ -168,7 +174,8 @@ Feature: provisioning
| phone | +4971125242890 |
| address | Foo Bar Town |
| website | https://nextcloud.com |
- | twitter | Nextcloud |
+ | twitter | Nextcloud |
+ | bluesky | nextcloud.bsky.social |
And sending "PUT" to "/cloud/users/brand-new-user" with
| key | organisation |
| value | Nextcloud GmbH |
@@ -212,6 +219,11 @@ Feature: provisioning
| value | v2-local |
Then the OCS status code should be "100"
And the HTTP status code should be "200"
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | blueskyScope |
+ | value | v2-local |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
When sending "PUT" to "/cloud/users/brand-new-user" with
| key | addressScope |
| value | v2-federated |
@@ -247,7 +259,8 @@ Feature: provisioning
Then user "brand-new-user" has
| id | brand-new-user |
| phoneScope | v2-private |
- | twitterScope | v2-local |
+ | twitterScope | v2-local |
+ | blueskyScope | v2-local |
| addressScope | v2-federated |
| emailScope | v2-published |
diff --git a/core/img/actions/bluesky.svg b/core/img/actions/bluesky.svg
new file mode 100644
index 00000000000..5c373b934ea
--- /dev/null
+++ b/core/img/actions/bluesky.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/core/img/bluesky.svg b/core/img/bluesky.svg
new file mode 100644
index 00000000000..5c373b934ea
--- /dev/null
+++ b/core/img/bluesky.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php
index 9c7c35d4a6b..d00b1d2e9a3 100644
--- a/lib/private/Accounts/AccountManager.php
+++ b/lib/private/Accounts/AccountManager.php
@@ -78,6 +78,7 @@ class AccountManager implements IAccountManager {
self::PROPERTY_PRONOUNS => self::SCOPE_FEDERATED,
self::PROPERTY_ROLE => self::SCOPE_LOCAL,
self::PROPERTY_TWITTER => self::SCOPE_LOCAL,
+ self::PROPERTY_BLUESKY => self::SCOPE_LOCAL,
self::PROPERTY_WEBSITE => self::SCOPE_LOCAL,
];
@@ -563,6 +564,13 @@ class AccountManager implements IAccountManager {
'verified' => self::NOT_VERIFIED,
],
+ [
+ 'name' => self::PROPERTY_BLUESKY,
+ 'value' => '',
+ 'scope' => $scopes[self::PROPERTY_BLUESKY],
+ 'verified' => self::NOT_VERIFIED,
+ ],
+
[
'name' => self::PROPERTY_FEDIVERSE,
'value' => '',
@@ -713,6 +721,47 @@ class AccountManager implements IAccountManager {
}
}
+ private function validateBlueSkyHandle(string $text): bool {
+ if ($text === '') {
+ return true;
+ }
+
+ $lowerText = strtolower($text);
+
+ if ($lowerText === 'bsky.social') {
+ // "bsky.social" itself is not a valid handle
+ return false;
+ }
+
+ if (str_ends_with($lowerText, '.bsky.social')) {
+ $parts = explode('.', $lowerText);
+
+ // Must be exactly: username.bsky.social → 3 parts
+ if (count($parts) !== 3 || $parts[1] !== 'bsky' || $parts[2] !== 'social') {
+ return false;
+ }
+
+ $username = $parts[0];
+
+ // Must be 3–18 chars, alphanumeric/hyphen, no start/end hyphen
+ return preg_match('/^[a-z0-9][a-z0-9-]{2,17}$/', $username) === 1;
+ }
+
+ // Allow custom domains (Bluesky handle via personal domain)
+ return filter_var($text, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
+ }
+
+
+ private function sanitizePropertyBluesky(IAccountProperty $property): void {
+ if ($property->getName() === self::PROPERTY_BLUESKY) {
+ if (!$this->validateBlueSkyHandle($property->getValue())) {
+ throw new InvalidArgumentException(self::PROPERTY_BLUESKY);
+ }
+
+ $property->setValue($property->getValue());
+ }
+ }
+
/**
* @throws InvalidArgumentException If the property value is not a valid fediverse handle (username@instance where instance is a valid domain)
*/
@@ -804,6 +853,15 @@ class AccountManager implements IAccountManager {
// valid case, nothing to do
}
+ try {
+ $property = $account->getProperty(self::PROPERTY_BLUESKY);
+ if ($property->getValue() !== '') {
+ $this->sanitizePropertyBluesky($property);
+ }
+ } catch (PropertyDoesNotExistException $e) {
+ // valid case, nothing to do
+ }
+
try {
$property = $account->getProperty(self::PROPERTY_FEDIVERSE);
if ($property->getValue() !== '') {
diff --git a/lib/private/Profile/Actions/BlueskyAction.php b/lib/private/Profile/Actions/BlueskyAction.php
new file mode 100644
index 00000000000..d05682aac1a
--- /dev/null
+++ b/lib/private/Profile/Actions/BlueskyAction.php
@@ -0,0 +1,65 @@
+accountManager->getAccount($targetUser);
+ $this->value = $account->getProperty(IAccountManager::PROPERTY_BLUESKY)->getValue();
+ }
+
+ public function getAppId(): string {
+ return 'core';
+ }
+
+ public function getId(): string {
+ return IAccountManager::PROPERTY_BLUESKY;
+ }
+
+ public function getDisplayId(): string {
+ return $this->l10nFactory->get('lib')->t('Bluesky');
+ }
+
+ public function getTitle(): string {
+ $displayUsername = $this->value;
+ return $this->l10nFactory->get('lib')->t('View %s on Bluesky', [$displayUsername]);
+ }
+
+ public function getPriority(): int {
+ return 60;
+ }
+
+ public function getIcon(): string {
+ return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'actions/bluesky.svg'));
+ }
+
+ public function getTarget(): ?string {
+ if (empty($this->value)) {
+ return null;
+ }
+ $username = $this->value;
+ return 'https://bsky.app/profile/' . $username;
+ }
+}
diff --git a/lib/private/Profile/ProfileManager.php b/lib/private/Profile/ProfileManager.php
index 1ade208fbcf..7c15ed614aa 100644
--- a/lib/private/Profile/ProfileManager.php
+++ b/lib/private/Profile/ProfileManager.php
@@ -14,6 +14,7 @@ use OC\Core\Db\ProfileConfig;
use OC\Core\Db\ProfileConfigMapper;
use OC\Core\ResponseDefinitions;
use OC\KnownUser\KnownUserService;
+use OC\Profile\Actions\BlueskyAction;
use OC\Profile\Actions\EmailAction;
use OC\Profile\Actions\FediverseAction;
use OC\Profile\Actions\PhoneAction;
@@ -56,6 +57,7 @@ class ProfileManager implements IProfileManager {
PhoneAction::class,
WebsiteAction::class,
TwitterAction::class,
+ BlueskyAction::class,
FediverseAction::class,
];
diff --git a/lib/public/Accounts/IAccountManager.php b/lib/public/Accounts/IAccountManager.php
index 92fc0002674..8c4148b5060 100644
--- a/lib/public/Accounts/IAccountManager.php
+++ b/lib/public/Accounts/IAccountManager.php
@@ -96,10 +96,15 @@ interface IAccountManager {
public const PROPERTY_ADDRESS = 'address';
/**
- * @since 15.0.0
+ * @deprecated 32.0.0
*/
public const PROPERTY_TWITTER = 'twitter';
+ /**
+ * @since 32.0.0
+ */
+ public const PROPERTY_BLUESKY = 'bluesky';
+
/**
* @since 26.0.0
*/
@@ -160,6 +165,7 @@ interface IAccountManager {
self::PROPERTY_PRONOUNS,
self::PROPERTY_ROLE,
self::PROPERTY_TWITTER,
+ self::PROPERTY_BLUESKY,
self::PROPERTY_WEBSITE,
];
diff --git a/lib/public/Profile/IProfileManager.php b/lib/public/Profile/IProfileManager.php
index f4e90e39d12..aec06fb4c86 100644
--- a/lib/public/Profile/IProfileManager.php
+++ b/lib/public/Profile/IProfileManager.php
@@ -55,6 +55,7 @@ interface IProfileManager {
IAccountManager::PROPERTY_EMAIL => self::VISIBILITY_SHOW_USERS_ONLY,
IAccountManager::PROPERTY_PHONE => self::VISIBILITY_SHOW_USERS_ONLY,
IAccountManager::PROPERTY_TWITTER => self::VISIBILITY_SHOW,
+ IAccountManager::PROPERTY_BLUESKY => self::VISIBILITY_SHOW,
IAccountManager::PROPERTY_WEBSITE => self::VISIBILITY_SHOW,
IAccountManager::PROPERTY_PRONOUNS => self::VISIBILITY_SHOW,
];
diff --git a/tests/lib/Accounts/AccountManagerTest.php b/tests/lib/Accounts/AccountManagerTest.php
index 97078467936..c625644bd96 100644
--- a/tests/lib/Accounts/AccountManagerTest.php
+++ b/tests/lib/Accounts/AccountManagerTest.php
@@ -574,6 +574,13 @@ class AccountManagerTest extends TestCase {
'verified' => IAccountManager::NOT_VERIFIED,
],
+ [
+ 'name' => IAccountManager::PROPERTY_BLUESKY,
+ 'value' => '',
+ 'scope' => IAccountManager::SCOPE_LOCAL,
+ 'verified' => IAccountManager::NOT_VERIFIED,
+ ],
+
[
'name' => IAccountManager::PROPERTY_FEDIVERSE,
'value' => '',
diff --git a/tests/lib/Accounts/AccountTest.php b/tests/lib/Accounts/AccountTest.php
index 514ff17e58e..ddba7c559c0 100644
--- a/tests/lib/Accounts/AccountTest.php
+++ b/tests/lib/Accounts/AccountTest.php
@@ -64,6 +64,7 @@ class AccountTest extends TestCase {
IAccountManager::PROPERTY_AVATAR => new AccountProperty(IAccountManager::PROPERTY_AVATAR, '', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),
IAccountManager::PROPERTY_PHONE => new AccountProperty(IAccountManager::PROPERTY_PHONE, '+358407991028', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED, ''),
IAccountManager::PROPERTY_TWITTER => new AccountProperty(IAccountManager::PROPERTY_TWITTER, 'therealsteve', IAccountManager::SCOPE_PRIVATE, IAccountManager::NOT_VERIFIED, ''),
+ IAccountManager::PROPERTY_BLUESKY => new AccountProperty(IAccountManager::PROPERTY_BLUESKY, 'therealsteve.bsky.social', IAccountManager::SCOPE_PRIVATE, IAccountManager::NOT_VERIFIED, ''),
IAccountManager::PROPERTY_ORGANISATION => new AccountProperty(IAccountManager::PROPERTY_ORGANISATION, 'Steve Incorporated', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
IAccountManager::PROPERTY_ROLE => new AccountProperty(IAccountManager::PROPERTY_ROLE, 'Founder', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''),
IAccountManager::PROPERTY_HEADLINE => new AccountProperty(IAccountManager::PROPERTY_HEADLINE, 'I am Steve', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''),