WIP Twilio
This commit is contained in:
		
							parent
							
								
									214efbde36
								
							
						
					
					
						commit
						cea434a57c
					
				
					 34 changed files with 311 additions and 143 deletions
				
			
		|  | @ -455,6 +455,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit | ||||||
| 		return s.ensureUser(s.withAccountSync(s.handleAccountPhoneNumberAdd))(w, r, v) | 		return s.ensureUser(s.withAccountSync(s.handleAccountPhoneNumberAdd))(w, r, v) | ||||||
| 	} else if r.Method == http.MethodPost && r.URL.Path == apiAccountPhonePath { | 	} else if r.Method == http.MethodPost && r.URL.Path == apiAccountPhonePath { | ||||||
| 		return s.ensureUser(s.withAccountSync(s.handleAccountPhoneNumberVerify))(w, r, v) | 		return s.ensureUser(s.withAccountSync(s.handleAccountPhoneNumberVerify))(w, r, v) | ||||||
|  | 	} else if r.Method == http.MethodDelete && r.URL.Path == apiAccountPhonePath { | ||||||
|  | 		return s.ensureUser(s.withAccountSync(s.handleAccountPhoneNumberDelete))(w, r, v) | ||||||
| 	} else if r.Method == http.MethodGet && r.URL.Path == apiStatsPath { | 	} else if r.Method == http.MethodGet && r.URL.Path == apiStatsPath { | ||||||
| 		return s.handleStats(w, r, v) | 		return s.handleStats(w, r, v) | ||||||
| 	} else if r.Method == http.MethodGet && r.URL.Path == apiTiersPath { | 	} else if r.Method == http.MethodGet && r.URL.Path == apiTiersPath { | ||||||
|  | @ -692,6 +694,9 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e | ||||||
| 	} else if call != "" && !vrate.CallAllowed() { | 	} else if call != "" && !vrate.CallAllowed() { | ||||||
| 		return nil, errHTTPTooManyRequestsLimitCalls.With(t) | 		return nil, errHTTPTooManyRequestsLimitCalls.With(t) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	// FIXME check allowed phone numbers | ||||||
|  | 	 | ||||||
| 	if m.PollID != "" { | 	if m.PollID != "" { | ||||||
| 		m = newPollRequestMessage(t.ID, m.PollID) | 		m = newPollRequestMessage(t.ID, m.PollID) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -146,13 +146,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		if len(phoneNumbers) > 0 { | 		if len(phoneNumbers) > 0 { | ||||||
| 			response.PhoneNumbers = make([]*apiAccountPhoneNumberResponse, 0) | 			response.PhoneNumbers = phoneNumbers | ||||||
| 			for _, p := range phoneNumbers { |  | ||||||
| 				response.PhoneNumbers = append(response.PhoneNumbers, &apiAccountPhoneNumberResponse{ |  | ||||||
| 					Number:   p.Number, |  | ||||||
| 					Verified: p.Verified, |  | ||||||
| 				}) |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		response.Username = user.Everyone | 		response.Username = user.Everyone | ||||||
|  | @ -542,19 +536,15 @@ func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Requ | ||||||
| 	} else if u.IsUser() && u.Tier.CallLimit == 0 { | 	} else if u.IsUser() && u.Tier.CallLimit == 0 { | ||||||
| 		return errHTTPUnauthorized | 		return errHTTPUnauthorized | ||||||
| 	} | 	} | ||||||
| 	// Actually add the unverified number, and send verification | 	// Check if phone number exists | ||||||
| 	logvr(v, r). | 	phoneNumbers, err := s.userManager.PhoneNumbers(u.ID) | ||||||
| 		Tag(tagAccount). | 	if err != nil { | ||||||
| 		Fields(log.Context{ | 		return err | ||||||
| 			"number": req.Number, | 	} else if util.Contains(phoneNumbers, req.Number) { | ||||||
| 		}). |  | ||||||
| 		Debug("Adding phone number, and sending verification") |  | ||||||
| 	if err := s.userManager.AddPhoneNumber(u.ID, req.Number); err != nil { |  | ||||||
| 		if err == user.ErrPhoneNumberExists { |  | ||||||
| 		return errHTTPConflictPhoneNumberExists | 		return errHTTPConflictPhoneNumberExists | ||||||
| 	} | 	} | ||||||
| 		return err | 	// Actually add the unverified number, and send verification | ||||||
| 	} | 	logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Sending phone number verification") | ||||||
| 	if err := s.verifyPhone(v, r, req.Number); err != nil { | 	if err := s.verifyPhone(v, r, req.Number); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | @ -570,31 +560,27 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R | ||||||
| 	if !phoneNumberRegex.MatchString(req.Number) { | 	if !phoneNumberRegex.MatchString(req.Number) { | ||||||
| 		return errHTTPBadRequestPhoneNumberInvalid | 		return errHTTPBadRequestPhoneNumberInvalid | ||||||
| 	} | 	} | ||||||
| 	// Get phone numbers, and check if it's in the list |  | ||||||
| 	phoneNumbers, err := s.userManager.PhoneNumbers(u.ID) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	found := false |  | ||||||
| 	for _, phoneNumber := range phoneNumbers { |  | ||||||
| 		if phoneNumber.Number == req.Number && !phoneNumber.Verified { |  | ||||||
| 			found = true |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	if !found { |  | ||||||
| 		return errHTTPBadRequestPhoneNumberInvalid |  | ||||||
| 	} |  | ||||||
| 	if err := s.checkVerifyPhone(v, r, req.Number, req.Code); err != nil { | 	if err := s.checkVerifyPhone(v, r, req.Number, req.Code); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	logvr(v, r). | 	logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Adding phone number as verified") | ||||||
| 		Tag(tagAccount). | 	if err := s.userManager.AddPhoneNumber(u.ID, req.Number); err != nil { | ||||||
| 		Fields(log.Context{ | 		return err | ||||||
| 			"number": req.Number, | 	} | ||||||
| 		}). | 	return s.writeJSON(w, newSuccessResponse()) | ||||||
| 		Debug("Marking phone number as verified") | } | ||||||
| 	if err := s.userManager.MarkPhoneNumberVerified(u.ID, req.Number); err != nil { | 
 | ||||||
|  | func (s *Server) handleAccountPhoneNumberDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||||
|  | 	u := v.User() | ||||||
|  | 	req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if !phoneNumberRegex.MatchString(req.Number) { | ||||||
|  | 		return errHTTPBadRequestPhoneNumberInvalid | ||||||
|  | 	} | ||||||
|  | 	logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Deleting phone number") | ||||||
|  | 	if err := s.userManager.DeletePhoneNumber(u.ID, req.Number); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	return s.writeJSON(w, newSuccessResponse()) | 	return s.writeJSON(w, newSuccessResponse()) | ||||||
|  |  | ||||||
|  | @ -282,11 +282,6 @@ type apiAccountPhoneNumberRequest struct { | ||||||
| 	Code   string `json:"code,omitempty"` // Only supplied in "verify" call | 	Code   string `json:"code,omitempty"` // Only supplied in "verify" call | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type apiAccountPhoneNumberResponse struct { |  | ||||||
| 	Number   string `json:"number"` |  | ||||||
| 	Verified bool   `json:"verified"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type apiAccountTier struct { | type apiAccountTier struct { | ||||||
| 	Code string `json:"code"` | 	Code string `json:"code"` | ||||||
| 	Name string `json:"name"` | 	Name string `json:"name"` | ||||||
|  | @ -344,7 +339,7 @@ type apiAccountResponse struct { | ||||||
| 	Subscriptions []*user.Subscription       `json:"subscriptions,omitempty"` | 	Subscriptions []*user.Subscription       `json:"subscriptions,omitempty"` | ||||||
| 	Reservations  []*apiAccountReservation   `json:"reservations,omitempty"` | 	Reservations  []*apiAccountReservation   `json:"reservations,omitempty"` | ||||||
| 	Tokens        []*apiAccountTokenResponse `json:"tokens,omitempty"` | 	Tokens        []*apiAccountTokenResponse `json:"tokens,omitempty"` | ||||||
| 	PhoneNumbers  []*apiAccountPhoneNumberResponse `json:"phone_numbers,omitempty"` | 	PhoneNumbers  []string                   `json:"phone_numbers,omitempty"` | ||||||
| 	Tier          *apiAccountTier            `json:"tier,omitempty"` | 	Tier          *apiAccountTier            `json:"tier,omitempty"` | ||||||
| 	Limits        *apiAccountLimits          `json:"limits,omitempty"` | 	Limits        *apiAccountLimits          `json:"limits,omitempty"` | ||||||
| 	Stats         *apiAccountStats           `json:"stats,omitempty"` | 	Stats         *apiAccountStats           `json:"stats,omitempty"` | ||||||
|  |  | ||||||
|  | @ -115,7 +115,6 @@ const ( | ||||||
| 		CREATE TABLE IF NOT EXISTS user_phone ( | 		CREATE TABLE IF NOT EXISTS user_phone ( | ||||||
| 			user_id TEXT NOT NULL, | 			user_id TEXT NOT NULL, | ||||||
| 			phone_number TEXT NOT NULL, | 			phone_number TEXT NOT NULL, | ||||||
| 			verified INT NOT NULL, |  | ||||||
| 			PRIMARY KEY (user_id, phone_number), | 			PRIMARY KEY (user_id, phone_number), | ||||||
| 			FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE | 			FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE | ||||||
| 		); | 		); | ||||||
|  | @ -268,9 +267,9 @@ const ( | ||||||
| 		) | 		) | ||||||
| 	` | 	` | ||||||
| 
 | 
 | ||||||
| 	selectPhoneNumbersQuery        = `SELECT phone_number, verified FROM user_phone WHERE user_id = ?` | 	selectPhoneNumbersQuery = `SELECT phone_number FROM user_phone WHERE user_id = ?` | ||||||
| 	insertPhoneNumberQuery         = `INSERT INTO user_phone (user_id, phone_number, verified) VALUES (?, ?, 0)` | 	insertPhoneNumberQuery  = `INSERT INTO user_phone (user_id, phone_number) VALUES (?, ?)` | ||||||
| 	updatePhoneNumberVerifiedQuery = `UPDATE user_phone SET verified=1 WHERE user_id = ? AND phone_number = ?` | 	deletePhoneNumberQuery  = `DELETE FROM user_phone WHERE user_id = ? AND phone_number = ?` | ||||||
| 
 | 
 | ||||||
| 	insertTierQuery = ` | 	insertTierQuery = ` | ||||||
| 		INSERT INTO tier (id, code, name, messages_limit, messages_expiry_duration, emails_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id) | 		INSERT INTO tier (id, code, name, messages_limit, messages_expiry_duration, emails_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id) | ||||||
|  | @ -414,7 +413,6 @@ const ( | ||||||
| 		CREATE TABLE IF NOT EXISTS user_phone ( | 		CREATE TABLE IF NOT EXISTS user_phone ( | ||||||
| 			user_id TEXT NOT NULL, | 			user_id TEXT NOT NULL, | ||||||
| 			phone_number TEXT NOT NULL, | 			phone_number TEXT NOT NULL, | ||||||
| 			verified INT NOT NULL, |  | ||||||
| 			PRIMARY KEY (user_id, phone_number), | 			PRIMARY KEY (user_id, phone_number), | ||||||
| 			FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE | 			FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE | ||||||
| 		); | 		); | ||||||
|  | @ -648,13 +646,14 @@ func (a *Manager) RemoveExpiredTokens() error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *Manager) PhoneNumbers(userID string) ([]*PhoneNumber, error) { | // PhoneNumbers returns all phone numbers for the user with the given user ID | ||||||
|  | func (a *Manager) PhoneNumbers(userID string) ([]string, error) { | ||||||
| 	rows, err := a.db.Query(selectPhoneNumbersQuery, userID) | 	rows, err := a.db.Query(selectPhoneNumbersQuery, userID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	defer rows.Close() | 	defer rows.Close() | ||||||
| 	phoneNumbers := make([]*PhoneNumber, 0) | 	phoneNumbers := make([]string, 0) | ||||||
| 	for { | 	for { | ||||||
| 		phoneNumber, err := a.readPhoneNumber(rows) | 		phoneNumber, err := a.readPhoneNumber(rows) | ||||||
| 		if err == ErrPhoneNumberNotFound { | 		if err == ErrPhoneNumberNotFound { | ||||||
|  | @ -667,23 +666,20 @@ func (a *Manager) PhoneNumbers(userID string) ([]*PhoneNumber, error) { | ||||||
| 	return phoneNumbers, nil | 	return phoneNumbers, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *Manager) readPhoneNumber(rows *sql.Rows) (*PhoneNumber, error) { | func (a *Manager) readPhoneNumber(rows *sql.Rows) (string, error) { | ||||||
| 	var phoneNumber string | 	var phoneNumber string | ||||||
| 	var verified bool |  | ||||||
| 	if !rows.Next() { | 	if !rows.Next() { | ||||||
| 		return nil, ErrPhoneNumberNotFound | 		return "", ErrPhoneNumberNotFound | ||||||
| 	} | 	} | ||||||
| 	if err := rows.Scan(&phoneNumber, &verified); err != nil { | 	if err := rows.Scan(&phoneNumber); err != nil { | ||||||
| 		return nil, err | 		return "", err | ||||||
| 	} else if err := rows.Err(); err != nil { | 	} else if err := rows.Err(); err != nil { | ||||||
| 		return nil, err | 		return "", err | ||||||
| 	} | 	} | ||||||
| 	return &PhoneNumber{ | 	return phoneNumber, nil | ||||||
| 		Number:   phoneNumber, |  | ||||||
| 		Verified: verified, |  | ||||||
| 	}, nil |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // AddPhoneNumber adds a phone number to the user with the given user ID | ||||||
| func (a *Manager) AddPhoneNumber(userID string, phoneNumber string) error { | func (a *Manager) AddPhoneNumber(userID string, phoneNumber string) error { | ||||||
| 	if _, err := a.db.Exec(insertPhoneNumberQuery, userID, phoneNumber); err != nil { | 	if _, err := a.db.Exec(insertPhoneNumberQuery, userID, phoneNumber); err != nil { | ||||||
| 		if sqliteErr, ok := err.(sqlite3.Error); ok && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique { | 		if sqliteErr, ok := err.(sqlite3.Error); ok && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique { | ||||||
|  | @ -694,12 +690,11 @@ func (a *Manager) AddPhoneNumber(userID string, phoneNumber string) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *Manager) MarkPhoneNumberVerified(userID string, phoneNumber string) error { | // DeletePhoneNumber deletes a phone number from the user with the given user ID | ||||||
| 	if _, err := a.db.Exec(updatePhoneNumberVerifiedQuery, userID, phoneNumber); err != nil { | func (a *Manager) DeletePhoneNumber(userID string, phoneNumber string) error { | ||||||
|  | 	_, err := a.db.Exec(deletePhoneNumberQuery, userID, phoneNumber) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| // RemoveDeletedUsers deletes all users that have been marked deleted for | // RemoveDeletedUsers deletes all users that have been marked deleted for | ||||||
| func (a *Manager) RemoveDeletedUsers() error { | func (a *Manager) RemoveDeletedUsers() error { | ||||||
|  |  | ||||||
|  | @ -71,11 +71,6 @@ type TokenUpdate struct { | ||||||
| 	LastOrigin netip.Addr | 	LastOrigin netip.Addr | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type PhoneNumber struct { |  | ||||||
| 	Number   string |  | ||||||
| 	Verified bool |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Prefs represents a user's configuration settings | // Prefs represents a user's configuration settings | ||||||
| type Prefs struct { | type Prefs struct { | ||||||
| 	Language      *string            `json:"language,omitempty"` | 	Language      *string            `json:"language,omitempty"` | ||||||
|  |  | ||||||
|  | @ -152,7 +152,7 @@ | ||||||
|     "publish_dialog_chip_delay_label": "تأخير التسليم", |     "publish_dialog_chip_delay_label": "تأخير التسليم", | ||||||
|     "subscribe_dialog_login_description": "هذا الموضوع محمي بكلمة مرور. الرجاء إدخال اسم المستخدم وكلمة المرور للاشتراك.", |     "subscribe_dialog_login_description": "هذا الموضوع محمي بكلمة مرور. الرجاء إدخال اسم المستخدم وكلمة المرور للاشتراك.", | ||||||
|     "subscribe_dialog_subscribe_button_cancel": "إلغاء", |     "subscribe_dialog_subscribe_button_cancel": "إلغاء", | ||||||
|     "subscribe_dialog_login_button_back": "العودة", |     "common_back": "العودة", | ||||||
|     "prefs_notifications_sound_play": "تشغيل الصوت المحدد", |     "prefs_notifications_sound_play": "تشغيل الصوت المحدد", | ||||||
|     "prefs_notifications_min_priority_title": "أولوية دنيا", |     "prefs_notifications_min_priority_title": "أولوية دنيا", | ||||||
|     "prefs_notifications_min_priority_max_only": "الأولوية القصوى فقط", |     "prefs_notifications_min_priority_max_only": "الأولوية القصوى فقط", | ||||||
|  | @ -225,7 +225,7 @@ | ||||||
|     "account_tokens_table_expires_header": "تنتهي مدة صلاحيته في", |     "account_tokens_table_expires_header": "تنتهي مدة صلاحيته في", | ||||||
|     "account_tokens_table_never_expires": "لا تنتهي صلاحيتها أبدا", |     "account_tokens_table_never_expires": "لا تنتهي صلاحيتها أبدا", | ||||||
|     "account_tokens_table_current_session": "جلسة المتصفح الحالية", |     "account_tokens_table_current_session": "جلسة المتصفح الحالية", | ||||||
|     "account_tokens_table_copy_to_clipboard": "انسخ إلى الحافظة", |     "common_copy_to_clipboard": "انسخ إلى الحافظة", | ||||||
|     "account_tokens_table_cannot_delete_or_edit": "لا يمكن تحرير أو حذف الرمز المميز للجلسة الحالية", |     "account_tokens_table_cannot_delete_or_edit": "لا يمكن تحرير أو حذف الرمز المميز للجلسة الحالية", | ||||||
|     "account_tokens_table_create_token_button": "إنشاء رمز مميز للوصول", |     "account_tokens_table_create_token_button": "إنشاء رمز مميز للوصول", | ||||||
|     "account_tokens_table_last_origin_tooltip": "من عنوان IP {{ip}}، انقر للبحث", |     "account_tokens_table_last_origin_tooltip": "من عنوان IP {{ip}}، انقر للبحث", | ||||||
|  |  | ||||||
|  | @ -104,7 +104,7 @@ | ||||||
|     "subscribe_dialog_subscribe_topic_placeholder": "Име на темата, напр. phils_alerts", |     "subscribe_dialog_subscribe_topic_placeholder": "Име на темата, напр. phils_alerts", | ||||||
|     "subscribe_dialog_subscribe_use_another_label": "Използване на друг сървър", |     "subscribe_dialog_subscribe_use_another_label": "Използване на друг сървър", | ||||||
|     "subscribe_dialog_login_username_label": "Потребител, напр. phil", |     "subscribe_dialog_login_username_label": "Потребител, напр. phil", | ||||||
|     "subscribe_dialog_login_button_back": "Назад", |     "common_back": "Назад", | ||||||
|     "subscribe_dialog_subscribe_button_cancel": "Отказ", |     "subscribe_dialog_subscribe_button_cancel": "Отказ", | ||||||
|     "subscribe_dialog_login_description": "Темата е защитена. За да се абонирате въведете потребител и парола.", |     "subscribe_dialog_login_description": "Темата е защитена. За да се абонирате въведете потребител и парола.", | ||||||
|     "subscribe_dialog_subscribe_button_subscribe": "Абониране", |     "subscribe_dialog_subscribe_button_subscribe": "Абониране", | ||||||
|  |  | ||||||
|  | @ -91,7 +91,7 @@ | ||||||
|     "subscribe_dialog_subscribe_button_subscribe": "Přihlásit odběr", |     "subscribe_dialog_subscribe_button_subscribe": "Přihlásit odběr", | ||||||
|     "subscribe_dialog_login_username_label": "Uživatelské jméno, např. phil", |     "subscribe_dialog_login_username_label": "Uživatelské jméno, např. phil", | ||||||
|     "subscribe_dialog_login_password_label": "Heslo", |     "subscribe_dialog_login_password_label": "Heslo", | ||||||
|     "subscribe_dialog_login_button_back": "Zpět", |     "common_back": "Zpět", | ||||||
|     "subscribe_dialog_login_button_login": "Přihlásit se", |     "subscribe_dialog_login_button_login": "Přihlásit se", | ||||||
|     "subscribe_dialog_error_user_not_authorized": "Uživatel {{username}} není autorizován", |     "subscribe_dialog_error_user_not_authorized": "Uživatel {{username}} není autorizován", | ||||||
|     "subscribe_dialog_error_user_anonymous": "anonymně", |     "subscribe_dialog_error_user_anonymous": "anonymně", | ||||||
|  | @ -305,7 +305,7 @@ | ||||||
|     "account_tokens_table_expires_header": "Vyprší", |     "account_tokens_table_expires_header": "Vyprší", | ||||||
|     "account_tokens_table_never_expires": "Nikdy nevyprší", |     "account_tokens_table_never_expires": "Nikdy nevyprší", | ||||||
|     "account_tokens_table_current_session": "Současná relace prohlížeče", |     "account_tokens_table_current_session": "Současná relace prohlížeče", | ||||||
|     "account_tokens_table_copy_to_clipboard": "Kopírování do schránky", |     "common_copy_to_clipboard": "Kopírování do schránky", | ||||||
|     "account_tokens_table_label_header": "Popisek", |     "account_tokens_table_label_header": "Popisek", | ||||||
|     "account_tokens_table_cannot_delete_or_edit": "Nelze upravit nebo odstranit aktuální token relace", |     "account_tokens_table_cannot_delete_or_edit": "Nelze upravit nebo odstranit aktuální token relace", | ||||||
|     "account_tokens_table_create_token_button": "Vytvořit přístupový token", |     "account_tokens_table_create_token_button": "Vytvořit přístupový token", | ||||||
|  |  | ||||||
|  | @ -91,7 +91,7 @@ | ||||||
|     "publish_dialog_delay_label": "Forsinkelse", |     "publish_dialog_delay_label": "Forsinkelse", | ||||||
|     "publish_dialog_button_send": "Send", |     "publish_dialog_button_send": "Send", | ||||||
|     "subscribe_dialog_subscribe_button_subscribe": "Tilmeld", |     "subscribe_dialog_subscribe_button_subscribe": "Tilmeld", | ||||||
|     "subscribe_dialog_login_button_back": "Tilbage", |     "common_back": "Tilbage", | ||||||
|     "subscribe_dialog_login_username_label": "Brugernavn, f.eks. phil", |     "subscribe_dialog_login_username_label": "Brugernavn, f.eks. phil", | ||||||
|     "account_basics_title": "Konto", |     "account_basics_title": "Konto", | ||||||
|     "subscribe_dialog_error_topic_already_reserved": "Emnet er allerede reserveret", |     "subscribe_dialog_error_topic_already_reserved": "Emnet er allerede reserveret", | ||||||
|  | @ -209,7 +209,7 @@ | ||||||
|     "subscribe_dialog_subscribe_use_another_label": "Brug en anden server", |     "subscribe_dialog_subscribe_use_another_label": "Brug en anden server", | ||||||
|     "account_basics_tier_upgrade_button": "Opgrader til Pro", |     "account_basics_tier_upgrade_button": "Opgrader til Pro", | ||||||
|     "account_upgrade_dialog_tier_features_messages_other": "{{messages}} daglige beskeder", |     "account_upgrade_dialog_tier_features_messages_other": "{{messages}} daglige beskeder", | ||||||
|     "account_tokens_table_copy_to_clipboard": "Kopier til udklipsholder", |     "common_copy_to_clipboard": "Kopier til udklipsholder", | ||||||
|     "prefs_reservations_edit_button": "Rediger emneadgang", |     "prefs_reservations_edit_button": "Rediger emneadgang", | ||||||
|     "account_upgrade_dialog_title": "Skift kontoniveau", |     "account_upgrade_dialog_title": "Skift kontoniveau", | ||||||
|     "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reserverede emner", |     "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reserverede emner", | ||||||
|  |  | ||||||
|  | @ -94,7 +94,7 @@ | ||||||
|     "publish_dialog_delay_placeholder": "Auslieferung verzögern, z.B. {{unixTimestamp}}, {{relativeTime}}, oder \"{{naturalLanguage}}\" (nur Englisch)", |     "publish_dialog_delay_placeholder": "Auslieferung verzögern, z.B. {{unixTimestamp}}, {{relativeTime}}, oder \"{{naturalLanguage}}\" (nur Englisch)", | ||||||
|     "prefs_appearance_title": "Darstellung", |     "prefs_appearance_title": "Darstellung", | ||||||
|     "subscribe_dialog_login_password_label": "Kennwort", |     "subscribe_dialog_login_password_label": "Kennwort", | ||||||
|     "subscribe_dialog_login_button_back": "Zurück", |     "common_back": "Zurück", | ||||||
|     "publish_dialog_chip_attach_url_label": "Datei von URL anhängen", |     "publish_dialog_chip_attach_url_label": "Datei von URL anhängen", | ||||||
|     "publish_dialog_chip_delay_label": "Auslieferung verzögern", |     "publish_dialog_chip_delay_label": "Auslieferung verzögern", | ||||||
|     "publish_dialog_chip_topic_label": "Thema ändern", |     "publish_dialog_chip_topic_label": "Thema ändern", | ||||||
|  | @ -284,7 +284,7 @@ | ||||||
|     "account_tokens_table_expires_header": "Verfällt", |     "account_tokens_table_expires_header": "Verfällt", | ||||||
|     "account_tokens_table_never_expires": "Verfällt nie", |     "account_tokens_table_never_expires": "Verfällt nie", | ||||||
|     "account_tokens_table_current_session": "Aktuelle Browser-Sitzung", |     "account_tokens_table_current_session": "Aktuelle Browser-Sitzung", | ||||||
|     "account_tokens_table_copy_to_clipboard": "In die Zwischenablage kopieren", |     "common_copy_to_clipboard": "In die Zwischenablage kopieren", | ||||||
|     "account_tokens_table_copied_to_clipboard": "Access-Token kopiert", |     "account_tokens_table_copied_to_clipboard": "Access-Token kopiert", | ||||||
|     "account_tokens_table_cannot_delete_or_edit": "Aktuelles Token kann nicht bearbeitet oder gelöscht werden", |     "account_tokens_table_cannot_delete_or_edit": "Aktuelles Token kann nicht bearbeitet oder gelöscht werden", | ||||||
|     "account_tokens_table_create_token_button": "Access-Token erzeugen", |     "account_tokens_table_create_token_button": "Access-Token erzeugen", | ||||||
|  |  | ||||||
|  | @ -2,6 +2,8 @@ | ||||||
|   "common_cancel": "Cancel", |   "common_cancel": "Cancel", | ||||||
|   "common_save": "Save", |   "common_save": "Save", | ||||||
|   "common_add": "Add", |   "common_add": "Add", | ||||||
|  |   "common_back": "Back", | ||||||
|  |   "common_copy_to_clipboard": "Copy to clipboard", | ||||||
|   "signup_title": "Create a ntfy account", |   "signup_title": "Create a ntfy account", | ||||||
|   "signup_form_username": "Username", |   "signup_form_username": "Username", | ||||||
|   "signup_form_password": "Password", |   "signup_form_password": "Password", | ||||||
|  | @ -169,7 +171,6 @@ | ||||||
|   "subscribe_dialog_login_description": "This topic is password-protected. Please enter username and password to subscribe.", |   "subscribe_dialog_login_description": "This topic is password-protected. Please enter username and password to subscribe.", | ||||||
|   "subscribe_dialog_login_username_label": "Username, e.g. phil", |   "subscribe_dialog_login_username_label": "Username, e.g. phil", | ||||||
|   "subscribe_dialog_login_password_label": "Password", |   "subscribe_dialog_login_password_label": "Password", | ||||||
|   "subscribe_dialog_login_button_back": "Back", |  | ||||||
|   "subscribe_dialog_login_button_login": "Login", |   "subscribe_dialog_login_button_login": "Login", | ||||||
|   "subscribe_dialog_error_user_not_authorized": "User {{username}} not authorized", |   "subscribe_dialog_error_user_not_authorized": "User {{username}} not authorized", | ||||||
|   "subscribe_dialog_error_topic_already_reserved": "Topic already reserved", |   "subscribe_dialog_error_topic_already_reserved": "Topic already reserved", | ||||||
|  | @ -187,7 +188,17 @@ | ||||||
|   "account_basics_password_dialog_button_submit": "Change password", |   "account_basics_password_dialog_button_submit": "Change password", | ||||||
|   "account_basics_password_dialog_current_password_incorrect": "Password incorrect", |   "account_basics_password_dialog_current_password_incorrect": "Password incorrect", | ||||||
|   "account_basics_phone_numbers_title": "Phone numbers", |   "account_basics_phone_numbers_title": "Phone numbers", | ||||||
|  |   "account_basics_phone_numbers_dialog_description": "To use the call notification feature, you need to add and verify at least one phone number. Adding it will send a verification SMS to your phone.", | ||||||
|   "account_basics_phone_numbers_description": "For phone call notifications", |   "account_basics_phone_numbers_description": "For phone call notifications", | ||||||
|  |   "account_basics_phone_numbers_no_phone_numbers_yet": "No phone numbers yet", | ||||||
|  |   "account_basics_phone_numbers_copied_to_clipboard": "Phone number copied to clipboard", | ||||||
|  |   "account_basics_phone_numbers_dialog_title": "Add phone number", | ||||||
|  |   "account_basics_phone_numbers_dialog_number_label": "Phone number", | ||||||
|  |   "account_basics_phone_numbers_dialog_number_placeholder": "e.g. +1222333444", | ||||||
|  |   "account_basics_phone_numbers_dialog_send_verification_button": "Send verification", | ||||||
|  |   "account_basics_phone_numbers_dialog_code_label": "Verification code", | ||||||
|  |   "account_basics_phone_numbers_dialog_code_placeholder": "e.g. 123456", | ||||||
|  |   "account_basics_phone_numbers_dialog_check_verification_button": "Confirm code", | ||||||
|   "account_usage_title": "Usage", |   "account_usage_title": "Usage", | ||||||
|   "account_usage_of_limit": "of {{limit}}", |   "account_usage_of_limit": "of {{limit}}", | ||||||
|   "account_usage_unlimited": "Unlimited", |   "account_usage_unlimited": "Unlimited", | ||||||
|  | @ -265,7 +276,6 @@ | ||||||
|   "account_tokens_table_expires_header": "Expires", |   "account_tokens_table_expires_header": "Expires", | ||||||
|   "account_tokens_table_never_expires": "Never expires", |   "account_tokens_table_never_expires": "Never expires", | ||||||
|   "account_tokens_table_current_session": "Current browser session", |   "account_tokens_table_current_session": "Current browser session", | ||||||
|   "account_tokens_table_copy_to_clipboard": "Copy to clipboard", |  | ||||||
|   "account_tokens_table_copied_to_clipboard": "Access token copied", |   "account_tokens_table_copied_to_clipboard": "Access token copied", | ||||||
|   "account_tokens_table_cannot_delete_or_edit": "Cannot edit or delete current session token", |   "account_tokens_table_cannot_delete_or_edit": "Cannot edit or delete current session token", | ||||||
|   "account_tokens_table_create_token_button": "Create access token", |   "account_tokens_table_create_token_button": "Create access token", | ||||||
|  |  | ||||||
|  | @ -81,7 +81,7 @@ | ||||||
|     "subscribe_dialog_login_description": "Este tópico está protegido por contraseña. Por favor, introduzca su nombre de usuario y contraseña para suscribirse.", |     "subscribe_dialog_login_description": "Este tópico está protegido por contraseña. Por favor, introduzca su nombre de usuario y contraseña para suscribirse.", | ||||||
|     "subscribe_dialog_login_username_label": "Nombre de usuario, ej. phil", |     "subscribe_dialog_login_username_label": "Nombre de usuario, ej. phil", | ||||||
|     "subscribe_dialog_login_password_label": "Contraseña", |     "subscribe_dialog_login_password_label": "Contraseña", | ||||||
|     "subscribe_dialog_login_button_back": "Volver", |     "common_back": "Volver", | ||||||
|     "subscribe_dialog_login_button_login": "Iniciar sesión", |     "subscribe_dialog_login_button_login": "Iniciar sesión", | ||||||
|     "subscribe_dialog_error_user_not_authorized": "Usuario {{username}} no autorizado", |     "subscribe_dialog_error_user_not_authorized": "Usuario {{username}} no autorizado", | ||||||
|     "subscribe_dialog_error_user_anonymous": "anónimo", |     "subscribe_dialog_error_user_anonymous": "anónimo", | ||||||
|  | @ -257,7 +257,7 @@ | ||||||
|     "account_tokens_table_expires_header": "Expira", |     "account_tokens_table_expires_header": "Expira", | ||||||
|     "account_tokens_table_never_expires": "Nunca expira", |     "account_tokens_table_never_expires": "Nunca expira", | ||||||
|     "account_tokens_table_current_session": "Sesión del navegador actual", |     "account_tokens_table_current_session": "Sesión del navegador actual", | ||||||
|     "account_tokens_table_copy_to_clipboard": "Copiar al portapapeles", |     "common_copy_to_clipboard": "Copiar al portapapeles", | ||||||
|     "account_tokens_table_copied_to_clipboard": "Token de acceso copiado", |     "account_tokens_table_copied_to_clipboard": "Token de acceso copiado", | ||||||
|     "account_tokens_table_cannot_delete_or_edit": "No se puede editar ni eliminar el token de sesión actual", |     "account_tokens_table_cannot_delete_or_edit": "No se puede editar ni eliminar el token de sesión actual", | ||||||
|     "account_tokens_table_create_token_button": "Crear token de acceso", |     "account_tokens_table_create_token_button": "Crear token de acceso", | ||||||
|  |  | ||||||
|  | @ -106,7 +106,7 @@ | ||||||
|     "prefs_notifications_title": "Notifications", |     "prefs_notifications_title": "Notifications", | ||||||
|     "prefs_notifications_delete_after_title": "Supprimer les notifications", |     "prefs_notifications_delete_after_title": "Supprimer les notifications", | ||||||
|     "prefs_users_add_button": "Ajouter un utilisateur", |     "prefs_users_add_button": "Ajouter un utilisateur", | ||||||
|     "subscribe_dialog_login_button_back": "Retour", |     "common_back": "Retour", | ||||||
|     "subscribe_dialog_error_user_anonymous": "anonyme", |     "subscribe_dialog_error_user_anonymous": "anonyme", | ||||||
|     "prefs_notifications_sound_no_sound": "Aucun son", |     "prefs_notifications_sound_no_sound": "Aucun son", | ||||||
|     "prefs_notifications_min_priority_title": "Priorité minimum", |     "prefs_notifications_min_priority_title": "Priorité minimum", | ||||||
|  | @ -293,7 +293,7 @@ | ||||||
|     "account_tokens_table_expires_header": "Expire", |     "account_tokens_table_expires_header": "Expire", | ||||||
|     "account_tokens_table_never_expires": "N'expire jamais", |     "account_tokens_table_never_expires": "N'expire jamais", | ||||||
|     "account_tokens_table_current_session": "Session de navigation actuelle", |     "account_tokens_table_current_session": "Session de navigation actuelle", | ||||||
|     "account_tokens_table_copy_to_clipboard": "Copier dans le presse-papier", |     "common_copy_to_clipboard": "Copier dans le presse-papier", | ||||||
|     "account_tokens_table_copied_to_clipboard": "Jeton d'accès copié", |     "account_tokens_table_copied_to_clipboard": "Jeton d'accès copié", | ||||||
|     "account_tokens_table_create_token_button": "Créer un jeton d'accès", |     "account_tokens_table_create_token_button": "Créer un jeton d'accès", | ||||||
|     "account_tokens_table_last_origin_tooltip": "Depuis l'adresse IP {{ip}}, cliquer pour rechercher", |     "account_tokens_table_last_origin_tooltip": "Depuis l'adresse IP {{ip}}, cliquer pour rechercher", | ||||||
|  |  | ||||||
|  | @ -84,7 +84,7 @@ | ||||||
|     "subscribe_dialog_login_description": "Ez a téma jelszóval védett. Jelentkezz be a feliratkozáshoz.", |     "subscribe_dialog_login_description": "Ez a téma jelszóval védett. Jelentkezz be a feliratkozáshoz.", | ||||||
|     "subscribe_dialog_login_username_label": "Felhasználónév, pl: jozsi", |     "subscribe_dialog_login_username_label": "Felhasználónév, pl: jozsi", | ||||||
|     "subscribe_dialog_login_password_label": "Jelszó", |     "subscribe_dialog_login_password_label": "Jelszó", | ||||||
|     "subscribe_dialog_login_button_back": "Vissza", |     "common_back": "Vissza", | ||||||
|     "subscribe_dialog_login_button_login": "Belépés", |     "subscribe_dialog_login_button_login": "Belépés", | ||||||
|     "subscribe_dialog_error_user_anonymous": "névtelen", |     "subscribe_dialog_error_user_anonymous": "névtelen", | ||||||
|     "subscribe_dialog_error_user_not_authorized": "A(z) {{username}} felhasználónak nincs hozzáférése", |     "subscribe_dialog_error_user_not_authorized": "A(z) {{username}} felhasználónak nincs hozzáférése", | ||||||
|  |  | ||||||
|  | @ -116,7 +116,7 @@ | ||||||
|     "common_save": "Simpan", |     "common_save": "Simpan", | ||||||
|     "prefs_appearance_title": "Tampilan", |     "prefs_appearance_title": "Tampilan", | ||||||
|     "subscribe_dialog_login_password_label": "Kata sandi", |     "subscribe_dialog_login_password_label": "Kata sandi", | ||||||
|     "subscribe_dialog_login_button_back": "Kembali", |     "common_back": "Kembali", | ||||||
|     "prefs_notifications_sound_title": "Suara notifikasi", |     "prefs_notifications_sound_title": "Suara notifikasi", | ||||||
|     "prefs_notifications_min_priority_low_and_higher": "Prioritas rendah dan lebih tinggi", |     "prefs_notifications_min_priority_low_and_higher": "Prioritas rendah dan lebih tinggi", | ||||||
|     "prefs_notifications_min_priority_default_and_higher": "Prioritas bawaan dan lebih tinggi", |     "prefs_notifications_min_priority_default_and_higher": "Prioritas bawaan dan lebih tinggi", | ||||||
|  | @ -278,7 +278,7 @@ | ||||||
|     "account_tokens_table_expires_header": "Kedaluwarsa", |     "account_tokens_table_expires_header": "Kedaluwarsa", | ||||||
|     "account_tokens_table_never_expires": "Tidak pernah kedaluwarsa", |     "account_tokens_table_never_expires": "Tidak pernah kedaluwarsa", | ||||||
|     "account_tokens_table_current_session": "Sesi peramban saat ini", |     "account_tokens_table_current_session": "Sesi peramban saat ini", | ||||||
|     "account_tokens_table_copy_to_clipboard": "Salin ke papan klip", |     "common_copy_to_clipboard": "Salin ke papan klip", | ||||||
|     "account_tokens_table_copied_to_clipboard": "Token akses disalin", |     "account_tokens_table_copied_to_clipboard": "Token akses disalin", | ||||||
|     "account_tokens_table_cannot_delete_or_edit": "Tidak dapat menyunting atau menghapus token sesi saat ini", |     "account_tokens_table_cannot_delete_or_edit": "Tidak dapat menyunting atau menghapus token sesi saat ini", | ||||||
|     "account_tokens_table_create_token_button": "Buat token akses", |     "account_tokens_table_create_token_button": "Buat token akses", | ||||||
|  |  | ||||||
|  | @ -178,7 +178,7 @@ | ||||||
|     "prefs_notifications_sound_play": "Riproduci il suono selezionato", |     "prefs_notifications_sound_play": "Riproduci il suono selezionato", | ||||||
|     "prefs_notifications_min_priority_title": "Priorità minima", |     "prefs_notifications_min_priority_title": "Priorità minima", | ||||||
|     "subscribe_dialog_login_description": "Questo argomento è protetto da password. Per favore inserisci username e password per iscriverti.", |     "subscribe_dialog_login_description": "Questo argomento è protetto da password. Per favore inserisci username e password per iscriverti.", | ||||||
|     "subscribe_dialog_login_button_back": "Indietro", |     "common_back": "Indietro", | ||||||
|     "subscribe_dialog_error_user_not_authorized": "Utente {{username}} non autorizzato", |     "subscribe_dialog_error_user_not_authorized": "Utente {{username}} non autorizzato", | ||||||
|     "prefs_notifications_title": "Notifiche", |     "prefs_notifications_title": "Notifiche", | ||||||
|     "prefs_notifications_delete_after_title": "Elimina le notifiche", |     "prefs_notifications_delete_after_title": "Elimina le notifiche", | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ | ||||||
|     "subscribe_dialog_login_description": "このトピックはログインする必要があります。ユーザー名とパスワードを入力してください。", |     "subscribe_dialog_login_description": "このトピックはログインする必要があります。ユーザー名とパスワードを入力してください。", | ||||||
|     "subscribe_dialog_login_username_label": "ユーザー名, 例) phil", |     "subscribe_dialog_login_username_label": "ユーザー名, 例) phil", | ||||||
|     "subscribe_dialog_login_password_label": "パスワード", |     "subscribe_dialog_login_password_label": "パスワード", | ||||||
|     "subscribe_dialog_login_button_back": "戻る", |     "common_back": "戻る", | ||||||
|     "subscribe_dialog_login_button_login": "ログイン", |     "subscribe_dialog_login_button_login": "ログイン", | ||||||
|     "prefs_notifications_min_priority_high_and_higher": "優先度高 およびそれ以上", |     "prefs_notifications_min_priority_high_and_higher": "優先度高 およびそれ以上", | ||||||
|     "prefs_notifications_min_priority_max_only": "優先度最高のみ", |     "prefs_notifications_min_priority_max_only": "優先度最高のみ", | ||||||
|  | @ -258,7 +258,7 @@ | ||||||
|     "account_tokens_table_expires_header": "期限", |     "account_tokens_table_expires_header": "期限", | ||||||
|     "account_tokens_table_never_expires": "無期限", |     "account_tokens_table_never_expires": "無期限", | ||||||
|     "account_tokens_table_current_session": "現在のブラウザセッション", |     "account_tokens_table_current_session": "現在のブラウザセッション", | ||||||
|     "account_tokens_table_copy_to_clipboard": "クリップボードにコピー", |     "common_copy_to_clipboard": "クリップボードにコピー", | ||||||
|     "account_tokens_table_copied_to_clipboard": "アクセストークンをコピーしました", |     "account_tokens_table_copied_to_clipboard": "アクセストークンをコピーしました", | ||||||
|     "account_tokens_table_cannot_delete_or_edit": "現在のセッショントークンは編集または削除できません", |     "account_tokens_table_cannot_delete_or_edit": "現在のセッショントークンは編集または削除できません", | ||||||
|     "account_tokens_table_create_token_button": "アクセストークンを生成", |     "account_tokens_table_create_token_button": "アクセストークンを生成", | ||||||
|  |  | ||||||
|  | @ -93,7 +93,7 @@ | ||||||
|     "subscribe_dialog_error_user_not_authorized": "사용자 {{username}} 은(는) 인증되지 않았습니다", |     "subscribe_dialog_error_user_not_authorized": "사용자 {{username}} 은(는) 인증되지 않았습니다", | ||||||
|     "subscribe_dialog_login_username_label": "사용자 이름, 예를 들면 phil", |     "subscribe_dialog_login_username_label": "사용자 이름, 예를 들면 phil", | ||||||
|     "subscribe_dialog_login_password_label": "비밀번호", |     "subscribe_dialog_login_password_label": "비밀번호", | ||||||
|     "subscribe_dialog_login_button_back": "뒤로가기", |     "common_back": "뒤로가기", | ||||||
|     "subscribe_dialog_login_button_login": "로그인", |     "subscribe_dialog_login_button_login": "로그인", | ||||||
|     "prefs_notifications_title": "알림", |     "prefs_notifications_title": "알림", | ||||||
|     "prefs_notifications_sound_title": "알림 효과음", |     "prefs_notifications_sound_title": "알림 효과음", | ||||||
|  |  | ||||||
|  | @ -113,7 +113,7 @@ | ||||||
|     "prefs_notifications_delete_after_one_week_description": "Merknader slettes automatisk etter én uke", |     "prefs_notifications_delete_after_one_week_description": "Merknader slettes automatisk etter én uke", | ||||||
|     "prefs_notifications_delete_after_one_month_description": "Merknader slettes automatisk etter én måned", |     "prefs_notifications_delete_after_one_month_description": "Merknader slettes automatisk etter én måned", | ||||||
|     "priority_min": "min.", |     "priority_min": "min.", | ||||||
|     "subscribe_dialog_login_button_back": "Tilbake", |     "common_back": "Tilbake", | ||||||
|     "prefs_notifications_delete_after_three_hours": "Etter tre timer", |     "prefs_notifications_delete_after_three_hours": "Etter tre timer", | ||||||
|     "prefs_users_table_base_url_header": "Tjeneste-nettadresse", |     "prefs_users_table_base_url_header": "Tjeneste-nettadresse", | ||||||
|     "common_cancel": "Avbryt", |     "common_cancel": "Avbryt", | ||||||
|  |  | ||||||
|  | @ -140,7 +140,7 @@ | ||||||
|     "subscribe_dialog_subscribe_title": "Onderwerp abonneren", |     "subscribe_dialog_subscribe_title": "Onderwerp abonneren", | ||||||
|     "subscribe_dialog_subscribe_description": "Onderwerpen zijn mogelijk niet beschermd met een wachtwoord, kies daarom een moeilijk te raden naam. Na abonneren kun je notificaties via PUT/POST sturen.", |     "subscribe_dialog_subscribe_description": "Onderwerpen zijn mogelijk niet beschermd met een wachtwoord, kies daarom een moeilijk te raden naam. Na abonneren kun je notificaties via PUT/POST sturen.", | ||||||
|     "subscribe_dialog_login_password_label": "Wachtwoord", |     "subscribe_dialog_login_password_label": "Wachtwoord", | ||||||
|     "subscribe_dialog_login_button_back": "Terug", |     "common_back": "Terug", | ||||||
|     "subscribe_dialog_login_button_login": "Aanmelden", |     "subscribe_dialog_login_button_login": "Aanmelden", | ||||||
|     "subscribe_dialog_error_user_not_authorized": "Gebruiker {{username}} heeft geen toegang", |     "subscribe_dialog_error_user_not_authorized": "Gebruiker {{username}} heeft geen toegang", | ||||||
|     "subscribe_dialog_error_user_anonymous": "anoniem", |     "subscribe_dialog_error_user_anonymous": "anoniem", | ||||||
|  | @ -331,7 +331,7 @@ | ||||||
|     "account_upgrade_dialog_button_cancel_subscription": "Abonnement opzeggen", |     "account_upgrade_dialog_button_cancel_subscription": "Abonnement opzeggen", | ||||||
|     "account_tokens_table_last_access_header": "Laatste toegang", |     "account_tokens_table_last_access_header": "Laatste toegang", | ||||||
|     "account_tokens_table_expires_header": "Verloopt op", |     "account_tokens_table_expires_header": "Verloopt op", | ||||||
|     "account_tokens_table_copy_to_clipboard": "Kopieer naar klembord", |     "common_copy_to_clipboard": "Kopieer naar klembord", | ||||||
|     "account_tokens_table_copied_to_clipboard": "Toegangstoken gekopieerd", |     "account_tokens_table_copied_to_clipboard": "Toegangstoken gekopieerd", | ||||||
|     "account_tokens_delete_dialog_submit_button": "Token definitief verwijderen", |     "account_tokens_delete_dialog_submit_button": "Token definitief verwijderen", | ||||||
|     "prefs_users_description_no_sync": "Gebruikers en wachtwoorden worden niet gesynchroniseerd met uw account.", |     "prefs_users_description_no_sync": "Gebruikers en wachtwoorden worden niet gesynchroniseerd met uw account.", | ||||||
|  |  | ||||||
|  | @ -107,7 +107,7 @@ | ||||||
|     "subscribe_dialog_login_username_label": "Nazwa użytkownika, np. phil", |     "subscribe_dialog_login_username_label": "Nazwa użytkownika, np. phil", | ||||||
|     "subscribe_dialog_login_password_label": "Hasło", |     "subscribe_dialog_login_password_label": "Hasło", | ||||||
|     "publish_dialog_button_cancel": "Anuluj", |     "publish_dialog_button_cancel": "Anuluj", | ||||||
|     "subscribe_dialog_login_button_back": "Powrót", |     "common_back": "Powrót", | ||||||
|     "subscribe_dialog_login_button_login": "Zaloguj się", |     "subscribe_dialog_login_button_login": "Zaloguj się", | ||||||
|     "subscribe_dialog_error_user_not_authorized": "Użytkownik {{username}} nie ma uprawnień", |     "subscribe_dialog_error_user_not_authorized": "Użytkownik {{username}} nie ma uprawnień", | ||||||
|     "subscribe_dialog_error_user_anonymous": "anonim", |     "subscribe_dialog_error_user_anonymous": "anonim", | ||||||
|  | @ -253,7 +253,7 @@ | ||||||
|     "account_tokens_table_expires_header": "Termin ważności", |     "account_tokens_table_expires_header": "Termin ważności", | ||||||
|     "account_tokens_table_never_expires": "Bezterminowy", |     "account_tokens_table_never_expires": "Bezterminowy", | ||||||
|     "account_tokens_table_current_session": "Aktualna sesja przeglądarki", |     "account_tokens_table_current_session": "Aktualna sesja przeglądarki", | ||||||
|     "account_tokens_table_copy_to_clipboard": "Kopiuj do schowka", |     "common_copy_to_clipboard": "Kopiuj do schowka", | ||||||
|     "account_tokens_table_copied_to_clipboard": "Token został skopiowany", |     "account_tokens_table_copied_to_clipboard": "Token został skopiowany", | ||||||
|     "account_tokens_table_cannot_delete_or_edit": "Nie można edytować ani usunąć tokenu aktualnej sesji", |     "account_tokens_table_cannot_delete_or_edit": "Nie można edytować ani usunąć tokenu aktualnej sesji", | ||||||
|     "account_tokens_table_create_token_button": "Utwórz token dostępowy", |     "account_tokens_table_create_token_button": "Utwórz token dostępowy", | ||||||
|  |  | ||||||
|  | @ -144,7 +144,7 @@ | ||||||
|     "subscribe_dialog_login_description": "Esse tópico é protegido por palavra-passe. Por favor insira um nome de utilizador e palavra-passe para subscrever.", |     "subscribe_dialog_login_description": "Esse tópico é protegido por palavra-passe. Por favor insira um nome de utilizador e palavra-passe para subscrever.", | ||||||
|     "subscribe_dialog_login_username_label": "Nome, por exemplo: \"filipe\"", |     "subscribe_dialog_login_username_label": "Nome, por exemplo: \"filipe\"", | ||||||
|     "subscribe_dialog_login_password_label": "Palavra-passe", |     "subscribe_dialog_login_password_label": "Palavra-passe", | ||||||
|     "subscribe_dialog_login_button_back": "Voltar", |     "common_back": "Voltar", | ||||||
|     "subscribe_dialog_login_button_login": "Autenticar", |     "subscribe_dialog_login_button_login": "Autenticar", | ||||||
|     "subscribe_dialog_error_user_anonymous": "anónimo", |     "subscribe_dialog_error_user_anonymous": "anónimo", | ||||||
|     "prefs_notifications_title": "Notificações", |     "prefs_notifications_title": "Notificações", | ||||||
|  |  | ||||||
|  | @ -93,7 +93,7 @@ | ||||||
|     "prefs_notifications_min_priority_low_and_higher": "Baixa prioridade e acima", |     "prefs_notifications_min_priority_low_and_higher": "Baixa prioridade e acima", | ||||||
|     "prefs_notifications_min_priority_default_and_higher": "Prioridade padrão e acima", |     "prefs_notifications_min_priority_default_and_higher": "Prioridade padrão e acima", | ||||||
|     "subscribe_dialog_login_password_label": "Senha", |     "subscribe_dialog_login_password_label": "Senha", | ||||||
|     "subscribe_dialog_login_button_back": "Voltar", |     "common_back": "Voltar", | ||||||
|     "prefs_notifications_min_priority_high_and_higher": "Alta prioridade e acima", |     "prefs_notifications_min_priority_high_and_higher": "Alta prioridade e acima", | ||||||
|     "prefs_notifications_min_priority_max_only": "Apenas prioridade máxima", |     "prefs_notifications_min_priority_max_only": "Apenas prioridade máxima", | ||||||
|     "prefs_notifications_delete_after_title": "Apagar notificações", |     "prefs_notifications_delete_after_title": "Apagar notificações", | ||||||
|  |  | ||||||
|  | @ -98,7 +98,7 @@ | ||||||
|     "subscribe_dialog_login_description": "Эта тема защищена паролем. Пожалуйста, введите имя пользователя и пароль, чтобы подписаться.", |     "subscribe_dialog_login_description": "Эта тема защищена паролем. Пожалуйста, введите имя пользователя и пароль, чтобы подписаться.", | ||||||
|     "subscribe_dialog_login_username_label": "Имя пользователя. Например, phil", |     "subscribe_dialog_login_username_label": "Имя пользователя. Например, phil", | ||||||
|     "subscribe_dialog_login_password_label": "Пароль", |     "subscribe_dialog_login_password_label": "Пароль", | ||||||
|     "subscribe_dialog_login_button_back": "Назад", |     "common_back": "Назад", | ||||||
|     "subscribe_dialog_login_button_login": "Войти", |     "subscribe_dialog_login_button_login": "Войти", | ||||||
|     "subscribe_dialog_error_user_not_authorized": "Пользователь {{username}} не авторизован", |     "subscribe_dialog_error_user_not_authorized": "Пользователь {{username}} не авторизован", | ||||||
|     "subscribe_dialog_error_user_anonymous": "анонимный пользователь", |     "subscribe_dialog_error_user_anonymous": "анонимный пользователь", | ||||||
|  | @ -206,7 +206,7 @@ | ||||||
|     "account_basics_tier_free": "Бесплатный", |     "account_basics_tier_free": "Бесплатный", | ||||||
|     "account_tokens_dialog_title_create": "Создать токен доступа", |     "account_tokens_dialog_title_create": "Создать токен доступа", | ||||||
|     "account_tokens_dialog_title_delete": "Удалить токен доступа", |     "account_tokens_dialog_title_delete": "Удалить токен доступа", | ||||||
|     "account_tokens_table_copy_to_clipboard": "Скопировать в буфер обмена", |     "common_copy_to_clipboard": "Скопировать в буфер обмена", | ||||||
|     "account_tokens_dialog_button_cancel": "Отмена", |     "account_tokens_dialog_button_cancel": "Отмена", | ||||||
|     "account_tokens_dialog_expires_unchanged": "Оставить срок истечения без изменений", |     "account_tokens_dialog_expires_unchanged": "Оставить срок истечения без изменений", | ||||||
|     "account_tokens_dialog_expires_x_days": "Токен истекает через {{days}} дней", |     "account_tokens_dialog_expires_x_days": "Токен истекает через {{days}} дней", | ||||||
|  |  | ||||||
|  | @ -95,14 +95,14 @@ | ||||||
|     "publish_dialog_email_placeholder": "Adress att vidarebefordra meddelandet till, t.ex. phil@example.com", |     "publish_dialog_email_placeholder": "Adress att vidarebefordra meddelandet till, t.ex. phil@example.com", | ||||||
|     "publish_dialog_details_examples_description": "Exempel och en detaljerad beskrivning av alla sändningsfunktioner finns i <docsLink>dokumentationen</docsLink> .", |     "publish_dialog_details_examples_description": "Exempel och en detaljerad beskrivning av alla sändningsfunktioner finns i <docsLink>dokumentationen</docsLink> .", | ||||||
|     "publish_dialog_button_send": "Skicka", |     "publish_dialog_button_send": "Skicka", | ||||||
|     "subscribe_dialog_login_button_back": "Tillbaka", |     "common_back": "Tillbaka", | ||||||
|     "account_basics_tier_free": "Gratis", |     "account_basics_tier_free": "Gratis", | ||||||
|     "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reserverat ämne", |     "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reserverat ämne", | ||||||
|     "account_delete_title": "Ta bort konto", |     "account_delete_title": "Ta bort konto", | ||||||
|     "account_upgrade_dialog_tier_features_messages_other": "{{messages}} dagliga meddelanden", |     "account_upgrade_dialog_tier_features_messages_other": "{{messages}} dagliga meddelanden", | ||||||
|     "account_upgrade_dialog_tier_features_emails_one": "{{emails}} dagligt e-postmeddelande", |     "account_upgrade_dialog_tier_features_emails_one": "{{emails}} dagligt e-postmeddelande", | ||||||
|     "account_upgrade_dialog_button_cancel": "Avbryt", |     "account_upgrade_dialog_button_cancel": "Avbryt", | ||||||
|     "account_tokens_table_copy_to_clipboard": "Kopiera till urklipp", |     "common_copy_to_clipboard": "Kopiera till urklipp", | ||||||
|     "account_tokens_table_copied_to_clipboard": "Åtkomsttoken kopierat", |     "account_tokens_table_copied_to_clipboard": "Åtkomsttoken kopierat", | ||||||
|     "account_tokens_description": "Använd åtkomsttoken när du publicerar och prenumererar via ntfy API, så att du inte behöver skicka dina kontouppgifter. Läs mer i <Link>dokumentationen</Link>.", |     "account_tokens_description": "Använd åtkomsttoken när du publicerar och prenumererar via ntfy API, så att du inte behöver skicka dina kontouppgifter. Läs mer i <Link>dokumentationen</Link>.", | ||||||
|     "account_tokens_table_create_token_button": "Skapa åtkomsttoken", |     "account_tokens_table_create_token_button": "Skapa åtkomsttoken", | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ | ||||||
|     "subscribe_dialog_login_description": "Bu konu parola korumalı. Abone olmak için lütfen kullanıcı adı ve parola girin.", |     "subscribe_dialog_login_description": "Bu konu parola korumalı. Abone olmak için lütfen kullanıcı adı ve parola girin.", | ||||||
|     "subscribe_dialog_login_username_label": "Kullanıcı adı, örn. phil", |     "subscribe_dialog_login_username_label": "Kullanıcı adı, örn. phil", | ||||||
|     "subscribe_dialog_login_password_label": "Parola", |     "subscribe_dialog_login_password_label": "Parola", | ||||||
|     "subscribe_dialog_login_button_back": "Geri", |     "common_back": "Geri", | ||||||
|     "subscribe_dialog_login_button_login": "Oturum aç", |     "subscribe_dialog_login_button_login": "Oturum aç", | ||||||
|     "subscribe_dialog_error_user_not_authorized": "{{username}} kullanıcısı yetkili değil", |     "subscribe_dialog_error_user_not_authorized": "{{username}} kullanıcısı yetkili değil", | ||||||
|     "subscribe_dialog_error_user_anonymous": "anonim", |     "subscribe_dialog_error_user_anonymous": "anonim", | ||||||
|  | @ -268,7 +268,7 @@ | ||||||
|     "account_tokens_table_token_header": "Belirteç", |     "account_tokens_table_token_header": "Belirteç", | ||||||
|     "account_tokens_table_label_header": "Etiket", |     "account_tokens_table_label_header": "Etiket", | ||||||
|     "account_tokens_table_current_session": "Geçerli tarayıcı oturumu", |     "account_tokens_table_current_session": "Geçerli tarayıcı oturumu", | ||||||
|     "account_tokens_table_copy_to_clipboard": "Panoya kopyala", |     "common_copy_to_clipboard": "Panoya kopyala", | ||||||
|     "account_tokens_table_copied_to_clipboard": "Erişim belirteci kopyalandı", |     "account_tokens_table_copied_to_clipboard": "Erişim belirteci kopyalandı", | ||||||
|     "account_tokens_table_cannot_delete_or_edit": "Geçerli oturum belirteci düzenlenemez veya silinemez", |     "account_tokens_table_cannot_delete_or_edit": "Geçerli oturum belirteci düzenlenemez veya silinemez", | ||||||
|     "account_tokens_table_create_token_button": "Erişim belirteci oluştur", |     "account_tokens_table_create_token_button": "Erişim belirteci oluştur", | ||||||
|  |  | ||||||
|  | @ -53,7 +53,7 @@ | ||||||
|     "subscribe_dialog_subscribe_use_another_label": "Використовувати інший сервер", |     "subscribe_dialog_subscribe_use_another_label": "Використовувати інший сервер", | ||||||
|     "subscribe_dialog_subscribe_base_url_label": "URL служби", |     "subscribe_dialog_subscribe_base_url_label": "URL служби", | ||||||
|     "subscribe_dialog_login_password_label": "Пароль", |     "subscribe_dialog_login_password_label": "Пароль", | ||||||
|     "subscribe_dialog_login_button_back": "Назад", |     "common_back": "Назад", | ||||||
|     "subscribe_dialog_error_user_not_authorized": "{{username}} користувач не авторизований", |     "subscribe_dialog_error_user_not_authorized": "{{username}} користувач не авторизований", | ||||||
|     "prefs_notifications_sound_description_none": "Сповіщення не відтворюють жодного звуку при надходженні", |     "prefs_notifications_sound_description_none": "Сповіщення не відтворюють жодного звуку при надходженні", | ||||||
|     "prefs_notifications_sound_description_some": "Сповіщення відтворюють звук {{sound}}", |     "prefs_notifications_sound_description_some": "Сповіщення відтворюють звук {{sound}}", | ||||||
|  |  | ||||||
|  | @ -103,7 +103,7 @@ | ||||||
|     "subscribe_dialog_login_description": "本主题受密码保护,请输入用户名和密码进行订阅。", |     "subscribe_dialog_login_description": "本主题受密码保护,请输入用户名和密码进行订阅。", | ||||||
|     "subscribe_dialog_login_username_label": "用户名,例如 phil", |     "subscribe_dialog_login_username_label": "用户名,例如 phil", | ||||||
|     "subscribe_dialog_login_password_label": "密码", |     "subscribe_dialog_login_password_label": "密码", | ||||||
|     "subscribe_dialog_login_button_back": "返回", |     "common_back": "返回", | ||||||
|     "subscribe_dialog_login_button_login": "登录", |     "subscribe_dialog_login_button_login": "登录", | ||||||
|     "subscribe_dialog_error_user_not_authorized": "未授权 {{username}} 用户", |     "subscribe_dialog_error_user_not_authorized": "未授权 {{username}} 用户", | ||||||
|     "subscribe_dialog_error_user_anonymous": "匿名", |     "subscribe_dialog_error_user_anonymous": "匿名", | ||||||
|  | @ -333,7 +333,7 @@ | ||||||
|     "account_tokens_table_expires_header": "过期", |     "account_tokens_table_expires_header": "过期", | ||||||
|     "account_tokens_table_never_expires": "永不过期", |     "account_tokens_table_never_expires": "永不过期", | ||||||
|     "account_tokens_table_current_session": "当前浏览器会话", |     "account_tokens_table_current_session": "当前浏览器会话", | ||||||
|     "account_tokens_table_copy_to_clipboard": "复制到剪贴板", |     "common_copy_to_clipboard": "复制到剪贴板", | ||||||
|     "account_tokens_table_copied_to_clipboard": "已复制访问令牌", |     "account_tokens_table_copied_to_clipboard": "已复制访问令牌", | ||||||
|     "account_tokens_table_cannot_delete_or_edit": "无法编辑或删除当前会话令牌", |     "account_tokens_table_cannot_delete_or_edit": "无法编辑或删除当前会话令牌", | ||||||
|     "account_tokens_table_create_token_button": "创建访问令牌", |     "account_tokens_table_create_token_button": "创建访问令牌", | ||||||
|  |  | ||||||
|  | @ -70,7 +70,7 @@ | ||||||
|     "subscribe_dialog_subscribe_button_subscribe": "訂閱", |     "subscribe_dialog_subscribe_button_subscribe": "訂閱", | ||||||
|     "emoji_picker_search_clear": "清除", |     "emoji_picker_search_clear": "清除", | ||||||
|     "subscribe_dialog_login_password_label": "密碼", |     "subscribe_dialog_login_password_label": "密碼", | ||||||
|     "subscribe_dialog_login_button_back": "返回", |     "common_back": "返回", | ||||||
|     "subscribe_dialog_login_button_login": "登入", |     "subscribe_dialog_login_button_login": "登入", | ||||||
|     "prefs_notifications_delete_after_never": "從不", |     "prefs_notifications_delete_after_never": "從不", | ||||||
|     "prefs_users_add_button": "新增使用者", |     "prefs_users_add_button": "新增使用者", | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { | import { | ||||||
|     accountBillingPortalUrl, |     accountBillingPortalUrl, | ||||||
|     accountBillingSubscriptionUrl, |     accountBillingSubscriptionUrl, | ||||||
|     accountPasswordUrl, |     accountPasswordUrl, accountPhoneUrl, | ||||||
|     accountReservationSingleUrl, |     accountReservationSingleUrl, | ||||||
|     accountReservationUrl, |     accountReservationUrl, | ||||||
|     accountSettingsUrl, |     accountSettingsUrl, | ||||||
|  | @ -299,6 +299,43 @@ class AccountApi { | ||||||
|         return await response.json(); // May throw SyntaxError
 |         return await response.json(); // May throw SyntaxError
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async verifyPhone(phoneNumber) { | ||||||
|  |         const url = accountPhoneUrl(config.base_url); | ||||||
|  |         console.log(`[AccountApi] Sending phone verification ${url}`); | ||||||
|  |         await fetchOrThrow(url, { | ||||||
|  |             method: "PUT", | ||||||
|  |             headers: withBearerAuth({}, session.token()), | ||||||
|  |             body: JSON.stringify({ | ||||||
|  |                 number: phoneNumber | ||||||
|  |             }) | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async checkVerifyPhone(phoneNumber, code) { | ||||||
|  |         const url = accountPhoneUrl(config.base_url); | ||||||
|  |         console.log(`[AccountApi] Checking phone verification code ${url}`); | ||||||
|  |         await fetchOrThrow(url, { | ||||||
|  |             method: "POST", | ||||||
|  |             headers: withBearerAuth({}, session.token()), | ||||||
|  |             body: JSON.stringify({ | ||||||
|  |                 number: phoneNumber, | ||||||
|  |                 code: code | ||||||
|  |             }) | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async deletePhoneNumber(phoneNumber, code) { | ||||||
|  |         const url = accountPhoneUrl(config.base_url); | ||||||
|  |         console.log(`[AccountApi] Deleting phone number ${url}`); | ||||||
|  |         await fetchOrThrow(url, { | ||||||
|  |             method: "DELETE", | ||||||
|  |             headers: withBearerAuth({}, session.token()), | ||||||
|  |             body: JSON.stringify({ | ||||||
|  |                 number: phoneNumber | ||||||
|  |             }) | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     async sync() { |     async sync() { | ||||||
|         try { |         try { | ||||||
|             if (!session.token()) { |             if (!session.token()) { | ||||||
|  |  | ||||||
|  | @ -27,6 +27,7 @@ export const accountReservationUrl = (baseUrl) => `${baseUrl}/v1/account/reserva | ||||||
| export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/account/reservation/${topic}`; | export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/account/reservation/${topic}`; | ||||||
| export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`; | export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`; | ||||||
| export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`; | export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`; | ||||||
|  | export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`; | ||||||
| export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`; | export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`; | ||||||
| export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); | export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); | ||||||
| export const expandUrl = (url) => [`https://${url}`, `http://${url}`]; | export const expandUrl = (url) => [`https://${url}`, `http://${url}`]; | ||||||
|  |  | ||||||
|  | @ -325,37 +325,183 @@ const AccountType = () => { | ||||||
| const PhoneNumbers = () => { | const PhoneNumbers = () => { | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|     const { account } = useContext(AccountContext); |     const { account } = useContext(AccountContext); | ||||||
|  |     const [dialogKey, setDialogKey] = useState(0); | ||||||
|  |     const [dialogOpen, setDialogOpen] = useState(false); | ||||||
|  |     const [snackOpen, setSnackOpen] = useState(false); | ||||||
|     const labelId = "prefPhoneNumbers"; |     const labelId = "prefPhoneNumbers"; | ||||||
| 
 | 
 | ||||||
|     const handleAdd = () => { |     const handleDialogOpen = () => { | ||||||
| 
 |         setDialogKey(prev => prev+1); | ||||||
|  |         setDialogOpen(true); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleClick = () => { |     const handleDialogClose = () => { | ||||||
| 
 |         setDialogOpen(false); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleDelete = () => { |     const handleCopy = (phoneNumber) => { | ||||||
| 
 |         navigator.clipboard.writeText(phoneNumber); | ||||||
|  |         setSnackOpen(true); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |     const handleDelete = async (phoneNumber) => { | ||||||
|  |         try { | ||||||
|  |             await accountApi.deletePhoneNumber(phoneNumber); | ||||||
|  |         } catch (e) { | ||||||
|  |             console.log(`[Account] Error deleting phone number`, e); | ||||||
|  |             if (e instanceof UnauthorizedError) { | ||||||
|  |                 session.resetAndRedirect(routes.login); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     if (!config.enable_calls) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     return ( |     return ( | ||||||
|         <Pref labelId={labelId} title={t("account_basics_phone_numbers_title")} description={t("account_basics_phone_numbers_description")}> |         <Pref labelId={labelId} title={t("account_basics_phone_numbers_title")} description={t("account_basics_phone_numbers_description")}> | ||||||
|             <div aria-labelledby={labelId}> |             <div aria-labelledby={labelId}> | ||||||
|                 {account?.phone_numbers.map(p => |                 {account?.phone_numbers?.map(phoneNumber => | ||||||
|                         <Chip |                         <Chip | ||||||
|                         label={p.number} |                             label={ | ||||||
|  |                                 <Tooltip title={t("common_copy_to_clipboard")}> | ||||||
|  |                                    <span>{phoneNumber}</span> | ||||||
|  |                                 </Tooltip> | ||||||
|  |                             } | ||||||
|                             variant="outlined" |                             variant="outlined" | ||||||
|                         onClick={() => navigator.clipboard.writeText(p.number)} |                             onClick={() => handleCopy(phoneNumber)} | ||||||
|                         onDelete={() => handleDelete(p.number)} |                             onDelete={() => handleDelete(phoneNumber)} | ||||||
|                         /> |                         /> | ||||||
|                 )} |                 )} | ||||||
|                 <IconButton onClick={() => handleAdd()}><AddIcon/></IconButton> |                 {!account?.phone_numbers && | ||||||
|  |                     <em>{t("account_basics_phone_numbers_no_phone_numbers_yet")}</em> | ||||||
|  |                 } | ||||||
|  |                 <IconButton onClick={handleDialogOpen}><AddIcon/></IconButton> | ||||||
|             </div> |             </div> | ||||||
|  |             <AddPhoneNumberDialog | ||||||
|  |                 key={`addPhoneNumberDialog${dialogKey}`} | ||||||
|  |                 open={dialogOpen} | ||||||
|  |                 onClose={handleDialogClose} | ||||||
|  |             /> | ||||||
|  |             <Portal> | ||||||
|  |                 <Snackbar | ||||||
|  |                     open={snackOpen} | ||||||
|  |                     autoHideDuration={3000} | ||||||
|  |                     onClose={() => setSnackOpen(false)} | ||||||
|  |                     message={t("account_basics_phone_numbers_copied_to_clipboard")} | ||||||
|  |                 /> | ||||||
|  |             </Portal> | ||||||
|         </Pref> |         </Pref> | ||||||
|     ) |     ) | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | const AddPhoneNumberDialog = (props) => { | ||||||
|  |     const { t } = useTranslation(); | ||||||
|  |     const [error, setError] = useState(""); | ||||||
|  |     const [phoneNumber, setPhoneNumber] = useState(""); | ||||||
|  |     const [code, setCode] = useState(""); | ||||||
|  |     const [sending, setSending] = useState(false); | ||||||
|  |     const [verificationCodeSent, setVerificationCodeSent] = useState(false); | ||||||
|  |     const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); | ||||||
|  | 
 | ||||||
|  |     const handleDialogSubmit = async () => { | ||||||
|  |         if (!verificationCodeSent) { | ||||||
|  |             await verifyPhone(); | ||||||
|  |         } else { | ||||||
|  |             await checkVerifyPhone(); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const handleCancel = () => { | ||||||
|  |         if (verificationCodeSent) { | ||||||
|  |             setVerificationCodeSent(false); | ||||||
|  |         } else { | ||||||
|  |             props.onClose(); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const verifyPhone = async () => { | ||||||
|  |         try { | ||||||
|  |             setSending(true); | ||||||
|  |             await accountApi.verifyPhone(phoneNumber); | ||||||
|  |             setVerificationCodeSent(true); | ||||||
|  |         } catch (e) { | ||||||
|  |             console.log(`[Account] Error sending verification`, e); | ||||||
|  |             if (e instanceof UnauthorizedError) { | ||||||
|  |                 session.resetAndRedirect(routes.login); | ||||||
|  |             } else { | ||||||
|  |                 setError(e.message); | ||||||
|  |             } | ||||||
|  |         } finally { | ||||||
|  |             setSending(false); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const checkVerifyPhone = async () => { | ||||||
|  |         try { | ||||||
|  |             setSending(true); | ||||||
|  |             await accountApi.checkVerifyPhone(phoneNumber, code); | ||||||
|  |             props.onClose(); | ||||||
|  |         } catch (e) { | ||||||
|  |             console.log(`[Account] Error confirming verification`, e); | ||||||
|  |             if (e instanceof UnauthorizedError) { | ||||||
|  |                 session.resetAndRedirect(routes.login); | ||||||
|  |             } else { | ||||||
|  |                 setError(e.message); | ||||||
|  |             } | ||||||
|  |         } finally { | ||||||
|  |             setSending(false); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}> | ||||||
|  |             <DialogTitle>{t("account_basics_phone_numbers_dialog_title")}</DialogTitle> | ||||||
|  |             <DialogContent> | ||||||
|  |                 <DialogContentText> | ||||||
|  |                     {t("account_basics_phone_numbers_dialog_description")} | ||||||
|  |                 </DialogContentText> | ||||||
|  |                 {!verificationCodeSent && | ||||||
|  |                     <TextField | ||||||
|  |                         margin="dense" | ||||||
|  |                         label={t("account_basics_phone_numbers_dialog_number_label")} | ||||||
|  |                         aria-label={t("account_basics_phone_numbers_dialog_number_label")} | ||||||
|  |                         placeholder={t("account_basics_phone_numbers_dialog_number_placeholder")} | ||||||
|  |                         type="tel" | ||||||
|  |                         value={phoneNumber} | ||||||
|  |                         onChange={ev => setPhoneNumber(ev.target.value)} | ||||||
|  |                         fullWidth | ||||||
|  |                         inputProps={{ inputMode: 'tel', pattern: '\+[0-9]*' }} | ||||||
|  |                         variant="standard" | ||||||
|  |                     /> | ||||||
|  |                 } | ||||||
|  |                 {verificationCodeSent && | ||||||
|  |                     <TextField | ||||||
|  |                         margin="dense" | ||||||
|  |                         label={t("account_basics_phone_numbers_dialog_code_label")} | ||||||
|  |                         aria-label={t("account_basics_phone_numbers_dialog_code_label")} | ||||||
|  |                         placeholder={t("account_basics_phone_numbers_dialog_code_placeholder")} | ||||||
|  |                         type="text" | ||||||
|  |                         value={code} | ||||||
|  |                         onChange={ev => setCode(ev.target.value)} | ||||||
|  |                         fullWidth | ||||||
|  |                         inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }} | ||||||
|  |                         variant="standard" | ||||||
|  |                     /> | ||||||
|  |                 } | ||||||
|  |             </DialogContent> | ||||||
|  |             <DialogFooter status={error}> | ||||||
|  |                 <Button onClick={handleCancel}>{verificationCodeSent ? t("common_back") : t("common_cancel")}</Button> | ||||||
|  |                 <Button onClick={handleDialogSubmit} disabled={sending || !/^\+\d+$/.test(phoneNumber)}> | ||||||
|  |                     {verificationCodeSent ?t("account_basics_phone_numbers_dialog_check_verification_button")  : t("account_basics_phone_numbers_dialog_send_verification_button")} | ||||||
|  |                 </Button> | ||||||
|  |             </DialogFooter> | ||||||
|  |         </Dialog> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| const Stats = () => { | const Stats = () => { | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|     const { account } = useContext(AccountContext); |     const { account } = useContext(AccountContext); | ||||||
|  | @ -594,7 +740,7 @@ const TokensTable = (props) => { | ||||||
|                             <span> |                             <span> | ||||||
|                                 <span style={{fontFamily: "Monospace", fontSize: "0.9rem"}}>{token.token.slice(0, 12)}</span> |                                 <span style={{fontFamily: "Monospace", fontSize: "0.9rem"}}>{token.token.slice(0, 12)}</span> | ||||||
|                                 ... |                                 ... | ||||||
|                                 <Tooltip title={t("account_tokens_table_copy_to_clipboard")} placement="right"> |                                 <Tooltip title={t("common_copy_to_clipboard")} placement="right"> | ||||||
|                                     <IconButton onClick={() => handleCopy(token.token)}><ContentCopy/></IconButton> |                                     <IconButton onClick={() => handleCopy(token.token)}><ContentCopy/></IconButton> | ||||||
|                                 </Tooltip> |                                 </Tooltip> | ||||||
|                             </span> |                             </span> | ||||||
|  |  | ||||||
|  | @ -288,7 +288,7 @@ const LoginPage = (props) => { | ||||||
|                 /> |                 /> | ||||||
|             </DialogContent> |             </DialogContent> | ||||||
|             <DialogFooter status={error}> |             <DialogFooter status={error}> | ||||||
|                 <Button onClick={props.onBack}>{t("subscribe_dialog_login_button_back")}</Button> |                 <Button onClick={props.onBack}>{t("common_back")}</Button> | ||||||
|                 <Button onClick={handleLogin}>{t("subscribe_dialog_login_button_login")}</Button> |                 <Button onClick={handleLogin}>{t("subscribe_dialog_login_button_login")}</Button> | ||||||
|             </DialogFooter> |             </DialogFooter> | ||||||
|         </> |         </> | ||||||
|  |  | ||||||
|  | @ -300,11 +300,9 @@ const TierCard = (props) => { | ||||||
|                             {tier.limits.reservations > 0 && <Feature>{t("account_upgrade_dialog_tier_features_reservations", { reservations: tier.limits.reservations, count: tier.limits.reservations })}</Feature>} |                             {tier.limits.reservations > 0 && <Feature>{t("account_upgrade_dialog_tier_features_reservations", { reservations: tier.limits.reservations, count: tier.limits.reservations })}</Feature>} | ||||||
|                             <Feature>{t("account_upgrade_dialog_tier_features_messages", { messages: formatNumber(tier.limits.messages), count: tier.limits.messages })}</Feature> |                             <Feature>{t("account_upgrade_dialog_tier_features_messages", { messages: formatNumber(tier.limits.messages), count: tier.limits.messages })}</Feature> | ||||||
|                             <Feature>{t("account_upgrade_dialog_tier_features_emails", { emails: formatNumber(tier.limits.emails), count: tier.limits.emails })}</Feature> |                             <Feature>{t("account_upgrade_dialog_tier_features_emails", { emails: formatNumber(tier.limits.emails), count: tier.limits.emails })}</Feature> | ||||||
|                             {tier.limits.sms > 0 && <Feature>{t("account_upgrade_dialog_tier_features_sms", { sms: formatNumber(tier.limits.sms), count: tier.limits.sms })}</Feature>} |  | ||||||
|                             {tier.limits.calls > 0 && <Feature>{t("account_upgrade_dialog_tier_features_calls", { calls: formatNumber(tier.limits.calls), count: tier.limits.calls })}</Feature>} |                             {tier.limits.calls > 0 && <Feature>{t("account_upgrade_dialog_tier_features_calls", { calls: formatNumber(tier.limits.calls), count: tier.limits.calls })}</Feature>} | ||||||
|                             <Feature>{t("account_upgrade_dialog_tier_features_attachment_file_size", { filesize: formatBytes(tier.limits.attachment_file_size, 0) })}</Feature> |                             <Feature>{t("account_upgrade_dialog_tier_features_attachment_file_size", { filesize: formatBytes(tier.limits.attachment_file_size, 0) })}</Feature> | ||||||
|                             {tier.limits.reservations === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_reservations")}</NoFeature>} |                             {tier.limits.reservations === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_reservations")}</NoFeature>} | ||||||
|                             {tier.limits.sms === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_sms")}</NoFeature>} |  | ||||||
|                             {tier.limits.calls === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_calls")}</NoFeature>} |                             {tier.limits.calls === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_calls")}</NoFeature>} | ||||||
|                         </List> |                         </List> | ||||||
|                         {tier.prices && props.interval === SubscriptionInterval.MONTH && |                         {tier.prices && props.interval === SubscriptionInterval.MONTH && | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue