aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMs Kimsible <1877318+kimsible@users.noreply.github.com>2021-08-26 08:22:33 +0200
committerGitHub <noreply@github.com>2021-08-26 08:22:33 +0200
commit7dca45f99db58d9bb3423a3765aaee68c69bc9f2 (patch)
tree6bdf45c304a58dca51a8e856a8d35238e7bc9bbb
parent8729a87024fc837f7dd6f13afeec90cf6dda1c06 (diff)
downloadPeerTube-7dca45f99db58d9bb3423a3765aaee68c69bc9f2.tar.gz
PeerTube-7dca45f99db58d9bb3423a3765aaee68c69bc9f2.tar.zst
PeerTube-7dca45f99db58d9bb3423a3765aaee68c69bc9f2.zip
Inform user to fill account profile and channels (#4352)
* Add account-setup modal when login * Add channels-setup alert into my-channels, my-playlists and upload page Co-authored-by: Ms Kimsible <kimsible@users.noreply.github.com>
-rw-r--r--CREDITS.md2
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.html2
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.html2
-rw-r--r--client/src/app/app.component.html5
-rw-r--r--client/src/app/app.component.ts2
-rw-r--r--client/src/app/app.module.ts2
-rw-r--r--client/src/app/modal/account-setup-modal.component.html33
-rw-r--r--client/src/app/modal/account-setup-modal.component.scss31
-rw-r--r--client/src/app/modal/account-setup-modal.component.ts70
-rw-r--r--client/src/app/shared/shared-icons/global-icon.component.ts1
-rw-r--r--client/src/app/shared/shared-main/misc/channels-setup-message.component.html8
-rw-r--r--client/src/app/shared/shared-main/misc/channels-setup-message.component.scss32
-rw-r--r--client/src/app/shared/shared-main/misc/channels-setup-message.component.ts32
-rw-r--r--client/src/app/shared/shared-main/misc/index.ts1
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts10
-rw-r--r--client/src/assets/images/misc/tip.svg1
17 files changed, 232 insertions, 4 deletions
diff --git a/CREDITS.md b/CREDITS.md
index 6cc369482..5de12d2cb 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -526,7 +526,7 @@
526# Icons 526# Icons
527 527
528 * [Feather Icons](https://feathericons.com) (MIT) 528 * [Feather Icons](https://feathericons.com) (MIT)
529 * `playlist add`, `history`, `subscriptions`, `miscellaneous-services.svg` by Material UI (Apache 2.0) 529 * `playlist add`, `history`, `subscriptions`, `miscellaneous-services.svg`, `tip` by Material UI (Apache 2.0)
530 * `support` by Chocobozzz (CC-BY) 530 * `support` by Chocobozzz (CC-BY)
531 * `language` by Aaron Jin (CC-BY) 531 * `language` by Aaron Jin (CC-BY)
532 * `video-language` by Rigel Kent (CC-BY) 532 * `video-language` by Rigel Kent (CC-BY)
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
index 9f139b4f2..4c5b46d5b 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
@@ -4,6 +4,8 @@
4 <span class="badge badge-secondary">{{ totalItems }}</span> 4 <span class="badge badge-secondary">{{ totalItems }}</span>
5</h1> 5</h1>
6 6
7<my-channels-setup-message [hideLink]="true"></my-channels-setup-message>
8
7<div class="video-channels-header d-flex justify-content-between"> 9<div class="video-channels-header d-flex justify-content-between">
8 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> 10 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
9 11
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
index 7f5b8bbf4..25b742bff 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
@@ -3,6 +3,8 @@
3 <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> 3 <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span>
4</h1> 4</h1>
5 5
6<my-channels-setup-message></my-channels-setup-message>
7
6<div class="video-playlists-header d-flex justify-content-between"> 8<div class="video-playlists-header d-flex justify-content-between">
7 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> 9 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
8 10
diff --git a/client/src/app/+videos/+video-edit/video-add.component.html b/client/src/app/+videos/+video-edit/video-add.component.html
index b056c6e7a..62b42ae10 100644
--- a/client/src/app/+videos/+video-edit/video-add.component.html
+++ b/client/src/app/+videos/+video-edit/video-add.component.html
@@ -45,6 +45,8 @@
45 <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container> 45 <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container>
46 </div> 46 </div>
47 47
48 <my-channels-setup-message></my-channels-setup-message>
49
48 <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" [ngClass]="{ 'hide-nav': !!secondStepType }"> 50 <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" [ngClass]="{ 'hide-nav': !!secondStepType }">
49 <ng-container ngbNavItem="upload"> 51 <ng-container ngbNavItem="upload">
50 <a ngbNavLink> 52 <a ngbNavLink>
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index 108b127be..da66e4ef0 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -60,9 +60,10 @@
60 </ng-template> 60 </ng-template>
61</p-toast> 61</p-toast>
62 62
63<ng-template [ngIf]="isUserLoggedIn()"> 63<ng-container *ngIf="isUserLoggedIn()">
64 <my-account-setup-modal #accountSetupModal></my-account-setup-modal>
64 <my-welcome-modal #welcomeModal></my-welcome-modal> 65 <my-welcome-modal #welcomeModal></my-welcome-modal>
65 <my-instance-config-warning-modal #instanceConfigWarningModal></my-instance-config-warning-modal> 66 <my-instance-config-warning-modal #instanceConfigWarningModal></my-instance-config-warning-modal>
66</ng-template> 67</ng-container>
67 68
68<my-custom-modal #customModal></my-custom-modal> 69<my-custom-modal #customModal></my-custom-modal>
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index dcc1f259f..4d5a9f75f 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -20,6 +20,7 @@ import { PluginService } from '@app/core/plugins/plugin.service'
20import { CustomModalComponent } from '@app/modal/custom-modal.component' 20import { CustomModalComponent } from '@app/modal/custom-modal.component'
21import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component' 21import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
22import { WelcomeModalComponent } from '@app/modal/welcome-modal.component' 22import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
23import { AccountSetupModalComponent } from '@app/modal/account-setup-modal.component'
23import { NgbConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap' 24import { NgbConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap'
24import { LoadingBarService } from '@ngx-loading-bar/core' 25import { LoadingBarService } from '@ngx-loading-bar/core'
25import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' 26import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
@@ -37,6 +38,7 @@ import { InstanceService } from './shared/shared-instance'
37export class AppComponent implements OnInit, AfterViewInit { 38export class AppComponent implements OnInit, AfterViewInit {
38 private static BROADCAST_MESSAGE_KEY = 'app-broadcast-message-dismissed' 39 private static BROADCAST_MESSAGE_KEY = 'app-broadcast-message-dismissed'
39 40
41 @ViewChild('accountSetupModal') accountSetupModal: AccountSetupModalComponent
40 @ViewChild('welcomeModal') welcomeModal: WelcomeModalComponent 42 @ViewChild('welcomeModal') welcomeModal: WelcomeModalComponent
41 @ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent 43 @ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent
42 @ViewChild('customModal') customModal: CustomModalComponent 44 @ViewChild('customModal') customModal: CustomModalComponent
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index dfdccbe69..8d9c39ad8 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -18,6 +18,7 @@ import { CustomModalComponent } from './modal/custom-modal.component'
18import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' 18import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component'
19import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component' 19import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component'
20import { WelcomeModalComponent } from './modal/welcome-modal.component' 20import { WelcomeModalComponent } from './modal/welcome-modal.component'
21import { AccountSetupModalComponent } from './modal/account-setup-modal.component'
21import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module' 22import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module'
22import { SharedFormModule } from './shared/shared-forms' 23import { SharedFormModule } from './shared/shared-forms'
23import { SharedGlobalIconModule } from './shared/shared-icons' 24import { SharedGlobalIconModule } from './shared/shared-icons'
@@ -53,6 +54,7 @@ export function loadConfigFactory (server: ServerService, pluginService: PluginS
53 SuggestionComponent, 54 SuggestionComponent,
54 HighlightPipe, 55 HighlightPipe,
55 56
57 AccountSetupModalComponent,
56 CustomModalComponent, 58 CustomModalComponent,
57 WelcomeModalComponent, 59 WelcomeModalComponent,
58 InstanceConfigWarningModalComponent, 60 InstanceConfigWarningModalComponent,
diff --git a/client/src/app/modal/account-setup-modal.component.html b/client/src/app/modal/account-setup-modal.component.html
new file mode 100644
index 000000000..55bae78bf
--- /dev/null
+++ b/client/src/app/modal/account-setup-modal.component.html
@@ -0,0 +1,33 @@
1<ng-template #modal let-hide="close">
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Welcome to {{ instanceName }}, dear user!</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div>
6
7 <div class="modal-body">
8 <img class="mascot" src="/client/assets/images/mascot/happy.svg" alt="mascot">
9
10 <div i18n class="subtitle">It's time to set up your account profile!</div>
11
12 <p i18n>Help moderators and other users to know <strong>who you are</strong> by:</p>
13
14 <ul>
15 <li *ngIf="!hasAccountAvatar" i18n>Uploading an <strong>avatar</strong></li>
16 <li *ngIf="!hasAccountDescription" i18n>Writing a <strong>description</strong></li>
17 </ul>
18 </div>
19
20 <div class="modal-footer inputs">
21 <input
22 type="button" role="button" i18n-value value="Remind me later" class="peertube-button grey-button"
23 (click)="hide()" (key.enter)="hide()"
24 >
25
26 <a i18n (click)="hide()" (key.enter)="hide()"
27 class="peertube-button-link orange-button" routerLink="/my-account"
28 rel="noopener noreferrer" ngbAutofocus>
29 Set up
30 </a>
31 </div>
32
33</ng-template>
diff --git a/client/src/app/modal/account-setup-modal.component.scss b/client/src/app/modal/account-setup-modal.component.scss
new file mode 100644
index 000000000..405f29d65
--- /dev/null
+++ b/client/src/app/modal/account-setup-modal.component.scss
@@ -0,0 +1,31 @@
1@use '_mixins' as *;
2@use '_variables' as *;
3
4.modal-body {
5 font-size: 15px;
6 display: flex;
7 flex-direction: column;
8 align-items: center;
9 justify-content: center;
10}
11
12.mascot-fw {
13 width: 170px;
14}
15
16.mascot {
17 @include margin-right(2rem);
18
19 display: block;
20 min-width: 170px;
21}
22
23.subtitle {
24 font-weight: $font-semibold;
25 margin-bottom: 10px;
26 font-size: 16px;
27}
28
29li {
30 margin-bottom: 10px;
31}
diff --git a/client/src/app/modal/account-setup-modal.component.ts b/client/src/app/modal/account-setup-modal.component.ts
new file mode 100644
index 000000000..e5d36e006
--- /dev/null
+++ b/client/src/app/modal/account-setup-modal.component.ts
@@ -0,0 +1,70 @@
1import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
2import { AuthService, ServerService, User } from '@app/core'
3import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
4import { HTMLServerConfig } from '@shared/models'
5
6@Component({
7 selector: 'my-account-setup-modal',
8 templateUrl: './account-setup-modal.component.html',
9 styleUrls: [ './account-setup-modal.component.scss' ]
10})
11export class AccountSetupModalComponent implements OnInit {
12 @ViewChild('modal', { static: true }) modal: ElementRef
13
14 user: User = null
15 ref: NgbModalRef = null
16
17 private serverConfig: HTMLServerConfig
18
19 constructor (
20 private authService: AuthService,
21 private modalService: NgbModal,
22 private serverService: ServerService
23 ) { }
24
25 get userInformationLoaded () {
26 return this.authService.userInformationLoaded
27 }
28
29 get instanceName () {
30 return this.serverConfig.instance.name
31 }
32
33 get isUserRoot () {
34 return this.user.username === 'root'
35 }
36
37 get hasAccountAvatar () {
38 return !!this.user.account.avatar
39 }
40
41 get hasAccountDescription () {
42 return !!this.user.account.description
43 }
44
45 ngOnInit () {
46 this.serverConfig = this.serverService.getHTMLConfig()
47 this.user = this.authService.getUser()
48
49 this.authService.userInformationLoaded
50 .subscribe(
51 () => {
52 if (this.isUserRoot) return false
53 if (this.hasAccountAvatar && this.hasAccountDescription) return false
54
55 this.show()
56 }
57 )
58 }
59
60 show () {
61 if (this.ref) return false
62
63 this.ref = this.modalService.open(this.modal, {
64 centered: true,
65 backdrop: 'static',
66 keyboard: false,
67 size: 'md'
68 })
69 }
70}
diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts
index 70d672306..a4c62c234 100644
--- a/client/src/app/shared/shared-icons/global-icon.component.ts
+++ b/client/src/app/shared/shared-icons/global-icon.component.ts
@@ -16,6 +16,7 @@ const icons = {
16 'playlist-add': require('!!raw-loader?!../../../assets/images/misc/playlist-add.svg').default, // material ui 16 'playlist-add': require('!!raw-loader?!../../../assets/images/misc/playlist-add.svg').default, // material ui
17 follower: require('!!raw-loader?!../../../assets/images/misc/account-arrow-left.svg').default, // material ui 17 follower: require('!!raw-loader?!../../../assets/images/misc/account-arrow-left.svg').default, // material ui
18 following: require('!!raw-loader?!../../../assets/images/misc/account-arrow-right.svg').default, // material ui 18 following: require('!!raw-loader?!../../../assets/images/misc/account-arrow-right.svg').default, // material ui
19 tip: require('!!raw-loader?!../../../assets/images/misc/tip.svg').default, // material ui
19 flame: require('!!raw-loader?!../../../assets/images/misc/flame.svg').default, 20 flame: require('!!raw-loader?!../../../assets/images/misc/flame.svg').default,
20 local: require('!!raw-loader?!../../../assets/images/misc/local.svg').default, 21 local: require('!!raw-loader?!../../../assets/images/misc/local.svg').default,
21 22
diff --git a/client/src/app/shared/shared-main/misc/channels-setup-message.component.html b/client/src/app/shared/shared-main/misc/channels-setup-message.component.html
new file mode 100644
index 000000000..3f94fa04c
--- /dev/null
+++ b/client/src/app/shared/shared-main/misc/channels-setup-message.component.html
@@ -0,0 +1,8 @@
1<div *ngIf="hasChannelNotConfigured" class="channels-setup-message alert alert-info">
2 <my-global-icon iconName="tip"></my-global-icon>
3
4 <div>
5 <div i18n>Some of your channels are not fully set up. Make them welcoming and explicit about what you publish by adding a <strong>banner</strong>, an <strong>avatar</strong> and a <strong>description</strong>.</div>
6 <a *ngIf="!hideLink" class="channels-settings-link" routerLink="/my-library/video-channels" i18n>Set up my channels</a>
7 </div>
8</div>
diff --git a/client/src/app/shared/shared-main/misc/channels-setup-message.component.scss b/client/src/app/shared/shared-main/misc/channels-setup-message.component.scss
new file mode 100644
index 000000000..7dcba2ca5
--- /dev/null
+++ b/client/src/app/shared/shared-main/misc/channels-setup-message.component.scss
@@ -0,0 +1,32 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4.channels-setup-message {
5 display: flex;
6 align-items: center;
7 justify-content: center;
8
9 my-global-icon {
10 width: 32px;
11 align-self: flex-start;
12
13 ::ng-deep {
14 svg {
15 fill: #0c5460;
16 }
17 }
18
19 + div {
20 margin-left: 10px;
21 text-align: center;
22
23 a.channels-settings-link {
24 @include peertube-button-link;
25 @include grey-button;
26
27 height: fit-content;
28 margin-top: 10px;
29 }
30 }
31 }
32}
diff --git a/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts b/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts
new file mode 100644
index 000000000..50b98841a
--- /dev/null
+++ b/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts
@@ -0,0 +1,32 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { AuthService, User } from '@app/core'
3import { VideoChannel } from '@app/shared/shared-main'
4
5@Component({
6 selector: 'my-channels-setup-message',
7 templateUrl: './channels-setup-message.component.html',
8 styleUrls: [ './channels-setup-message.component.scss' ]
9})
10export class ChannelsSetupMessageComponent implements OnInit {
11 @Input() hideLink = false
12
13 user: User = null
14
15 constructor (
16 private authService: AuthService
17 ) {}
18
19 get userInformationLoaded () {
20 return this.authService.userInformationLoaded
21 }
22
23 get hasChannelNotConfigured () {
24 return this.user.videoChannels
25 .filter((channel: VideoChannel) => (!channel.avatar || !channel.description))
26 .length > 0
27 }
28
29 ngOnInit () {
30 this.user = this.authService.getUser()
31 }
32}
diff --git a/client/src/app/shared/shared-main/misc/index.ts b/client/src/app/shared/shared-main/misc/index.ts
index dc8ef9754..24134d7cd 100644
--- a/client/src/app/shared/shared-main/misc/index.ts
+++ b/client/src/app/shared/shared-main/misc/index.ts
@@ -1,3 +1,4 @@
1export * from './channels-setup-message.component'
1export * from './help.component' 2export * from './help.component'
2export * from './list-overflow.component' 3export * from './list-overflow.component'
3export * from './top-menu-dropdown.component' 4export * from './top-menu-dropdown.component'
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index e5dfc59b2..80d0a84f3 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -34,7 +34,13 @@ import { CustomPageService } from './custom-page'
34import { DateToggleComponent } from './date' 34import { DateToggleComponent } from './date'
35import { FeedComponent } from './feeds' 35import { FeedComponent } from './feeds'
36import { LoaderComponent, SmallLoaderComponent } from './loaders' 36import { LoaderComponent, SmallLoaderComponent } from './loaders'
37import { HelpComponent, ListOverflowComponent, SimpleSearchInputComponent, TopMenuDropdownComponent } from './misc' 37import {
38 ChannelsSetupMessageComponent,
39 HelpComponent,
40 ListOverflowComponent,
41 SimpleSearchInputComponent,
42 TopMenuDropdownComponent
43} from './misc'
38import { PluginPlaceholderComponent } from './plugins' 44import { PluginPlaceholderComponent } from './plugins'
39import { ActorRedirectGuard } from './router' 45import { ActorRedirectGuard } from './router'
40import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' 46import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
@@ -91,6 +97,7 @@ import { VideoChannelService } from './video-channel'
91 LoaderComponent, 97 LoaderComponent,
92 SmallLoaderComponent, 98 SmallLoaderComponent,
93 99
100 ChannelsSetupMessageComponent,
94 HelpComponent, 101 HelpComponent,
95 ListOverflowComponent, 102 ListOverflowComponent,
96 TopMenuDropdownComponent, 103 TopMenuDropdownComponent,
@@ -146,6 +153,7 @@ import { VideoChannelService } from './video-channel'
146 LoaderComponent, 153 LoaderComponent,
147 SmallLoaderComponent, 154 SmallLoaderComponent,
148 155
156 ChannelsSetupMessageComponent,
149 HelpComponent, 157 HelpComponent,
150 ListOverflowComponent, 158 ListOverflowComponent,
151 TopMenuDropdownComponent, 159 TopMenuDropdownComponent,
diff --git a/client/src/assets/images/misc/tip.svg b/client/src/assets/images/misc/tip.svg
new file mode 100644
index 000000000..4b7d7543c
--- /dev/null
+++ b/client/src/assets/images/misc/tip.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><path d="M0,0h24v24H0V0z" fill="none"/></g><g><g><rect height="3" width="2" x="11" y="19"/><rect height="2" width="3" x="2" y="11"/><rect height="2" width="3" x="19" y="11"/><rect height="3" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -7.6665 17.8014)" width="1.99" x="16.66" y="16.66"/><rect height="1.99" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -10.9791 9.8041)" width="3" x="4.85" y="17.16"/><path d="M15,8.02V3H9v5.02C7.79,8.94,7,10.37,7,12c0,2.76,2.24,5,5,5s5-2.24,5-5C17,10.37,16.21,8.94,15,8.02z M11,5h2v2.1 C12.68,7.04,12.34,7,12,7s-0.68,0.04-1,0.1V5z"/></g></g></svg>