From 368e6d9ef5ebd47f781281fdc65bdb91a823d98b Mon Sep 17 00:00:00 2001 From: Dennis Schubert Date: Thu, 27 Oct 2016 02:29:14 +0200 Subject: [PATCH 01/71] Start 0.6.2.0 cycle [ci skip] --- Changelog.md | 8 ++++++++ config/defaults.yml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 9b2b19728..947abacd3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,11 @@ +# 0.6.2.0 + +## Refactor + +## Bug fixes + +## Features + # 0.6.1.0 Note: Although this is a minor release, the configuration file changed because the old Mapbox implementation is no longer valid, and the current implementation requires additional fields. Chances are high that if you're using the old integration, it will be broken anyway. If you do use Mapbox, please check out the `diaspora.yml.example` for new parameters. diff --git a/config/defaults.yml b/config/defaults.yml index 24dbe5432..9ca81debd 100644 --- a/config/defaults.yml +++ b/config/defaults.yml @@ -4,7 +4,7 @@ defaults: version: - number: "0.6.0.99" # Do not touch unless doing a release, do not backport the version number that's in master + number: "0.6.1.99" # Do not touch unless doing a release, do not backport the version number that's in master heroku: false environment: url: "http://localhost:3000/" From 16cd4752cb063eb9c957108a471a65deabae949d Mon Sep 17 00:00:00 2001 From: Benjamin Neff Date: Thu, 27 Oct 2016 02:43:01 +0200 Subject: [PATCH 02/71] Move auth_token to users controller This token is only used for the chat, it isn't an official API. --- app/assets/javascripts/jsxc.js | 2 +- app/controllers/api/v1/tokens_controller.rb | 16 ---------------- app/controllers/users_controller.rb | 5 +++++ config/routes.rb | 7 +------ 4 files changed, 7 insertions(+), 23 deletions(-) delete mode 100644 app/controllers/api/v1/tokens_controller.rb diff --git a/app/assets/javascripts/jsxc.js b/app/assets/javascripts/jsxc.js index 9209fd7bf..9e90634ca 100644 --- a/app/assets/javascripts/jsxc.js +++ b/app/assets/javascripts/jsxc.js @@ -12,7 +12,7 @@ // initialize jsxc xmpp client $(document).ready(function() { if (app.currentUser.authenticated()) { - $.post('api/v1/tokens', null, function(data) { + $.post("/user/auth_token", null, function(data) { if (jsxc && data['token']) { var jid = app.currentUser.get('diaspora_id'); jsxc.init({ diff --git a/app/controllers/api/v1/tokens_controller.rb b/app/controllers/api/v1/tokens_controller.rb deleted file mode 100644 index f59c2eac5..000000000 --- a/app/controllers/api/v1/tokens_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -class Api::V1::TokensController < ApplicationController - skip_before_filter :verify_authenticity_token - before_filter :authenticate_user! - - respond_to :json - - def create - current_user.ensure_authentication_token! - render :status => 200, :json => { :token => current_user.authentication_token } - end - - def destroy - current_user.reset_authentication_token! - render :json => true, :status => 200 - end -end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8e01dff62..958353e28 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -128,6 +128,11 @@ class UsersController < ApplicationController redirect_to edit_user_path end + def auth_token + current_user.ensure_authentication_token! + render status: 200, json: {token: current_user.authentication_token} + end + private # rubocop:disable Metrics/MethodLength diff --git a/config/routes.rb b/config/routes.rb index 4e7b9d498..02b15089c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -106,6 +106,7 @@ Diaspora::Application.routes.draw do get :download_profile post :export_photos get :download_photos + post :auth_token end controller :users do @@ -184,12 +185,6 @@ Diaspora::Application.routes.draw do end end - namespace :api do - namespace :v1 do - resources :tokens, :only => [:create, :destroy] - end - end - get 'community_spotlight' => "contacts#spotlight", :as => 'community_spotlight' # Mobile site From d421e42ddbd691b873f503d5b967f241ab6ac0e7 Mon Sep 17 00:00:00 2001 From: Benjamin Neff Date: Thu, 27 Oct 2016 04:07:25 +0200 Subject: [PATCH 03/71] Remove ability to authenticate with auth_token on the frontend Remove devise-token_authenticatable gem and only generate a token to be used by the chat. --- Gemfile | 1 - Gemfile.lock | 3 -- app/models/user.rb | 3 +- app/models/user/authentication_token.rb | 26 ++++++++++++ spec/models/user/authentication_token_spec.rb | 42 +++++++++++++++++++ 5 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 app/models/user/authentication_token.rb create mode 100644 spec/models/user/authentication_token_spec.rb diff --git a/Gemfile b/Gemfile index 5e99577d4..e4614cfd7 100644 --- a/Gemfile +++ b/Gemfile @@ -25,7 +25,6 @@ gem "json-schema", "2.7.0" gem "devise", "4.2.0" gem "devise_lastseenable", "0.0.6" -gem "devise-token_authenticatable", "0.5.2" # Captcha diff --git a/Gemfile.lock b/Gemfile.lock index 0196e5d41..73f20f63e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -171,8 +171,6 @@ GEM railties (>= 4.1.0, < 5.1) responders warden (~> 1.2.3) - devise-token_authenticatable (0.5.2) - devise (>= 4.0.0, < 4.3.0) devise_lastseenable (0.0.6) devise rails (>= 3.0.4) @@ -931,7 +929,6 @@ DEPENDENCIES cucumber-rails (= 1.4.5) database_cleaner (= 1.5.3) devise (= 4.2.0) - devise-token_authenticatable (= 0.5.2) devise_lastseenable (= 0.0.6) diaspora-prosody-config (= 0.0.7) diaspora_federation-rails (= 0.1.5) diff --git a/app/models/user.rb b/app/models/user.rb index 77cdd8d21..e8a09a322 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,6 +3,7 @@ # the COPYRIGHT file. class User < ActiveRecord::Base + include AuthenticationToken include Connecting include Querying include SocialActions @@ -16,7 +17,7 @@ class User < ActiveRecord::Base scope :halfyear_actives, ->(time = Time.now) { logged_in_since(time - 6.month) } scope :active, -> { joins(:person).where(people: {closed_account: false}) } - devise :token_authenticatable, :database_authenticatable, :registerable, + devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :lockable, :lastseenable, :lock_strategy => :none, :unlock_strategy => :none diff --git a/app/models/user/authentication_token.rb b/app/models/user/authentication_token.rb new file mode 100644 index 000000000..ca72950b4 --- /dev/null +++ b/app/models/user/authentication_token.rb @@ -0,0 +1,26 @@ +class User + module AuthenticationToken + extend ActiveSupport::Concern + + # Generate new authentication token and save the record. + def reset_authentication_token! + self.authentication_token = self.class.authentication_token + save(validate: false) + end + + # Generate authentication token unless already exists and save the record. + def ensure_authentication_token! + reset_authentication_token! if authentication_token.blank? + end + + module ClassMethods + # Generate a token checking if one does not already exist in the database. + def authentication_token + loop do + token = Devise.friendly_token(30) + break token unless User.exists?(authentication_token: token) + end + end + end + end +end diff --git a/spec/models/user/authentication_token_spec.rb b/spec/models/user/authentication_token_spec.rb new file mode 100644 index 000000000..b4b4c2304 --- /dev/null +++ b/spec/models/user/authentication_token_spec.rb @@ -0,0 +1,42 @@ +require "spec_helper" + +describe User::AuthenticationToken, type: :model do + describe "#reset_authentication_token!" do + it "sets the authentication token" do + expect(alice.authentication_token).to be_nil + alice.reset_authentication_token! + expect(alice.authentication_token).not_to be_nil + end + + it "resets the authentication token" do + alice.reset_authentication_token! + expect { alice.reset_authentication_token! }.to change { alice.authentication_token } + end + end + + describe "#ensure_authentication_token!" do + it "doesn't change the authentication token" do + alice.reset_authentication_token! + expect { alice.ensure_authentication_token! }.to_not change { alice.authentication_token } + end + + it "sets the authentication token if not yet set" do + expect(alice.authentication_token).to be_nil + alice.ensure_authentication_token! + expect(alice.authentication_token).not_to be_nil + end + end + + describe ".authentication_token" do + it "generates an authentication token" do + expect(User.authentication_token.length).to eq(30) + end + + it "checks that the authentication token is not yet in use by another user" do + alice.reset_authentication_token! + expect(Devise).to receive(:friendly_token).with(30).and_return(alice.authentication_token, "some_unused_token") + + expect(User.authentication_token).to eq("some_unused_token") + end + end +end From 9f38a424e7a71c3321dd24e4f1f1e5cf9a3bc63a Mon Sep 17 00:00:00 2001 From: Benjamin Neff Date: Thu, 27 Oct 2016 04:13:59 +0200 Subject: [PATCH 04/71] Revert "Test token authentication; should allow it" It shouldn't be allowed! This reverts commit 46097ba8c883ef51ed4159a9271536c720664dab. closes #7160 --- spec/controllers/users_controller_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index c73872d10..0d4a31b16 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -242,11 +242,11 @@ describe UsersController, :type => :controller do expect(assigns[:email_prefs]['mentioned']).to be false end - it 'does allow token auth' do + it "does not allow token auth" do sign_out :user bob.reset_authentication_token! get :edit, :auth_token => bob.authentication_token - expect(response.status).to eq(200) + expect(response).to redirect_to new_user_session_path end end From 2ec45317a3fa7ffa4ee1593a8a85d43f37079be7 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Wed, 2 Nov 2016 11:02:52 +0100 Subject: [PATCH 05/71] Add new JSON endpoint for reshares --- app/controllers/reshares_controller.rb | 12 ++++++ app/models/reshare.rb | 8 ++++ config/routes.rb | 1 + spec/controllers/reshares_controller_spec.rb | 40 ++++++++++++++++++++ 4 files changed, 61 insertions(+) diff --git a/app/controllers/reshares_controller.rb b/app/controllers/reshares_controller.rb index 22915891a..385e6acfc 100644 --- a/app/controllers/reshares_controller.rb +++ b/app/controllers/reshares_controller.rb @@ -17,4 +17,16 @@ class ResharesController < ApplicationController render :nothing => true, :status => 422 end end + + def index + @reshares = target.reshares.includes(author: :profile) + render json: @reshares.as_api_response(:backbone) + end + + private + + def target + @target ||= current_user.find_visible_shareable_by_id(Post, params[:post_id]) || + raise(ActiveRecord::RecordNotFound.new) + end end diff --git a/app/models/reshare.rb b/app/models/reshare.rb index bb914d401..a88208248 100644 --- a/app/models/reshare.rb +++ b/app/models/reshare.rb @@ -21,6 +21,14 @@ class Reshare < Post self.root.update_reshares_counter if self.root.present? end + acts_as_api + api_accessible :backbone do |t| + t.add :id + t.add :guid + t.add :author + t.add :created_at + end + def root_diaspora_id root.try(:author).try(:diaspora_handle) end diff --git a/config/routes.rb b/config/routes.rb index 02b15089c..190ad6b98 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,6 +38,7 @@ Diaspora::Application.routes.draw do resources :poll_participations, only: :create resources :likes, only: %i(create destroy index) resources :comments, only: %i(new create destroy index) + resources :reshares, only: :index end get 'p/:id' => 'posts#show', :as => 'short_post' diff --git a/spec/controllers/reshares_controller_spec.rb b/spec/controllers/reshares_controller_spec.rb index cc4b7e9ec..d33c93536 100644 --- a/spec/controllers/reshares_controller_spec.rb +++ b/spec/controllers/reshares_controller_spec.rb @@ -64,4 +64,44 @@ describe ResharesController, :type => :controller do end end end + + describe "#index" do + context "with a private post" do + before do + @alices_aspect = alice.aspects.where(name: "generic").first + @post = alice.post(:status_message, text: "hey", to: @alices_aspect.id) + end + + it "returns a 404 for a post not visible to the user" do + sign_in(eve, scope: :user) + expect { + get :index, post_id: @post.id, format: :json + }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "returns an empty array for a post visible to the user" do + sign_in(bob, scope: :user) + get :index, post_id: @post.id, format: :json + expect(JSON.parse(response.body)).to eq([]) + end + end + + context "with a public post" do + before do + sign_in(alice, scope: :user) + @post = alice.post(:status_message, text: "hey", public: true) + end + + it "returns an array of reshares for a post" do + bob.reshare!(@post) + get :index, post_id: @post.id, format: :json + expect(JSON.parse(response.body).map {|h| h["id"] }).to eq(@post.reshares.map(&:id)) + end + + it "returns an empty array for a post with no likes" do + get :index, post_id: @post.id, format: :json + expect(JSON.parse(response.body)).to eq([]) + end + end + end end From 98b345305e42483ffbdfc257b2a0e68c291bcd97 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Wed, 2 Nov 2016 11:06:52 +0100 Subject: [PATCH 06/71] Remove unused jasmine fixture --- spec/controllers/likes_controller_spec.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/spec/controllers/likes_controller_spec.rb b/spec/controllers/likes_controller_spec.rb index 066bdc01a..606cf69db 100644 --- a/spec/controllers/likes_controller_spec.rb +++ b/spec/controllers/likes_controller_spec.rb @@ -101,12 +101,6 @@ describe LikesController, :type => :controller do @message = alice.comment!(@message, "hey") if class_const == Comment end - it 'generates a jasmine fixture', :fixture => true do - get :index, id_field => @message.id - - save_fixture(response.body, "ajax_likes_on_#{class_const.to_s.underscore}") - end - it 'returns a 404 for a post not visible to the user' do sign_in eve expect{get :index, id_field => @message.id}.to raise_error(ActiveRecord::RecordNotFound) From e3b3da404f3609ae98ba0dc9d2f6eb97625660d1 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Wed, 2 Nov 2016 13:59:18 +0100 Subject: [PATCH 07/71] Fetch comments, likes and reshares separately Fixes #7165 closes #7167 --- Changelog.md | 1 + .../javascripts/app/collections/reshares.js | 5 ++++- .../javascripts/app/models/post/interactions.js | 4 ++++ .../javascripts/app/views/likes_info_view.js | 12 +++++------- .../javascripts/app/views/reshares_info_view.js | 12 +++++------- features/desktop/comments.feature | 13 +++++++++++++ features/step_definitions/like_steps.rb | 5 +++++ spec/controllers/reshares_controller_spec.rb | 2 +- .../app/models/post/interacations_spec.js | 16 +++++++++++++++- .../app/views/likes_info_view_spec.js | 14 +++++++++----- .../app/views/reshares_info_view_spec.js | 14 +++++++++----- 11 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 features/step_definitions/like_steps.rb diff --git a/Changelog.md b/Changelog.md index 6bcb60cbb..b6fd95c33 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,6 +3,7 @@ ## Refactor ## Bug fixes +* Fix fetching comments after fetching likes [#7167](https://github.com/diaspora/diaspora/pull/7167) ## Features diff --git a/app/assets/javascripts/app/collections/reshares.js b/app/assets/javascripts/app/collections/reshares.js index 1aee51053..ab7bb9173 100644 --- a/app/assets/javascripts/app/collections/reshares.js +++ b/app/assets/javascripts/app/collections/reshares.js @@ -2,6 +2,9 @@ app.collections.Reshares = Backbone.Collection.extend({ model: app.models.Reshare, - url : "/reshares" + + initialize: function(models, options) { + this.url = "/posts/" + options.post.id + "/reshares"; + } }); // @license-end diff --git a/app/assets/javascripts/app/models/post/interactions.js b/app/assets/javascripts/app/models/post/interactions.js index 28d6f3a57..51b40227e 100644 --- a/app/assets/javascripts/app/models/post/interactions.js +++ b/app/assets/javascripts/app/models/post/interactions.js @@ -70,6 +70,7 @@ app.models.Post.Interactions = Backbone.Model.extend({ self.post.set({participation: true}); self.trigger("change"); self.set({"likes_count" : self.get("likes_count") + 1}); + self.likes.trigger("change"); }, error: function() { app.flashMessages.error(Diaspora.I18n.t("failed_to_like")); @@ -84,6 +85,7 @@ app.models.Post.Interactions = Backbone.Model.extend({ this.userLike().destroy({success : function() { self.trigger('change'); self.set({"likes_count" : self.get("likes_count") - 1}); + self.likes.trigger("change"); }}); app.instrument("track", "Unlike"); @@ -116,6 +118,8 @@ app.models.Post.Interactions = Backbone.Model.extend({ app.stream.addNow(reshare); } interactions.trigger("change"); + interactions.set({"reshares_count": interactions.get("reshares_count") + 1}); + interactions.reshares.trigger("change"); }) .fail(function(){ app.flashMessages.error(Diaspora.I18n.t("reshares.duplicate")); diff --git a/app/assets/javascripts/app/views/likes_info_view.js b/app/assets/javascripts/app/views/likes_info_view.js index 9300ac9ad..d511c2ef9 100644 --- a/app/assets/javascripts/app/views/likes_info_view.js +++ b/app/assets/javascripts/app/views/likes_info_view.js @@ -11,7 +11,7 @@ app.views.LikesInfo = app.views.Base.extend({ tooltipSelector : ".avatar", initialize : function() { - this.model.interactions.bind('change', this.render, this); + this.model.interactions.likes.on("change", this.render, this); this.displayAvatars = false; }, @@ -19,18 +19,16 @@ app.views.LikesInfo = app.views.Base.extend({ return _.extend(this.defaultPresenter(), { likes : this.model.interactions.likes.toJSON(), likesCount : this.model.interactions.likesCount(), - displayAvatars : this.model.interactions.get("fetched") && this.displayAvatars + displayAvatars: this.displayAvatars }); }, showAvatars : function(evt){ if(evt) { evt.preventDefault() } this.displayAvatars = true; - if(!this.model.interactions.get("fetched")){ - this.model.interactions.fetch(); - } else { - this.model.interactions.trigger("change"); - } + this.model.interactions.likes.fetch({success: function() { + this.model.interactions.likes.trigger("change"); + }.bind(this)}); } }); // @license-end diff --git a/app/assets/javascripts/app/views/reshares_info_view.js b/app/assets/javascripts/app/views/reshares_info_view.js index b91027e84..a1d985dd4 100644 --- a/app/assets/javascripts/app/views/reshares_info_view.js +++ b/app/assets/javascripts/app/views/reshares_info_view.js @@ -11,7 +11,7 @@ app.views.ResharesInfo = app.views.Base.extend({ tooltipSelector : ".avatar", initialize : function() { - this.model.interactions.bind("change", this.render, this); + this.model.interactions.reshares.bind("change", this.render, this); this.displayAvatars = false; }, @@ -19,18 +19,16 @@ app.views.ResharesInfo = app.views.Base.extend({ return _.extend(this.defaultPresenter(), { reshares : this.model.interactions.reshares.toJSON(), resharesCount : this.model.interactions.resharesCount(), - displayAvatars : this.model.interactions.get("fetched") && this.displayAvatars + displayAvatars: this.displayAvatars }); }, showAvatars : function(evt){ if(evt) { evt.preventDefault() } this.displayAvatars = true; - if(!this.model.interactions.get("fetched")){ - this.model.interactions.fetch(); - } else { - this.model.interactions.trigger("change"); - } + this.model.interactions.reshares.fetch({success: function() { + this.model.interactions.reshares.trigger("change"); + }.bind(this)}); } }); // @license-end diff --git a/features/desktop/comments.feature b/features/desktop/comments.feature index 3b9fb1aa2..0ef2ca6c8 100644 --- a/features/desktop/comments.feature +++ b/features/desktop/comments.feature @@ -73,3 +73,16 @@ Feature: commenting When I follow "less than a minute ago" within ".comments .comment:last-child" Then I should see "I think that’s a cat" within ".comments .comment .highlighted" And I should have scrolled down + + Scenario: Show more comments after loading likes + When "alice@alice.alice" has commented a lot on "Look at this dog" + And "alice@alice.alice" has liked the post "Look at this dog" + And I am on "alice@alice.alice"'s page + Then I should see "Look at this dog" + And I should not see "Comment 2" + + When I follow "1 Like" + Then I should not see "1 Like" + + When I click on selector ".toggle_post_comments" + Then I should see "Comment 2" diff --git a/features/step_definitions/like_steps.rb b/features/step_definitions/like_steps.rb new file mode 100644 index 000000000..e0670e7c8 --- /dev/null +++ b/features/step_definitions/like_steps.rb @@ -0,0 +1,5 @@ +Given /^"([^"]*)" has liked the post "([^"]*)"$/ do |email, post_text| + user = User.find_by(email: email) + post = StatusMessage.find_by(text: post_text) + user.like!(post) +end diff --git a/spec/controllers/reshares_controller_spec.rb b/spec/controllers/reshares_controller_spec.rb index d33c93536..571f5651b 100644 --- a/spec/controllers/reshares_controller_spec.rb +++ b/spec/controllers/reshares_controller_spec.rb @@ -98,7 +98,7 @@ describe ResharesController, :type => :controller do expect(JSON.parse(response.body).map {|h| h["id"] }).to eq(@post.reshares.map(&:id)) end - it "returns an empty array for a post with no likes" do + it "returns an empty array for a post with no reshares" do get :index, post_id: @post.id, format: :json expect(JSON.parse(response.body)).to eq([]) end diff --git a/spec/javascripts/app/models/post/interacations_spec.js b/spec/javascripts/app/models/post/interacations_spec.js index 6b2b66e37..0f7b9926c 100644 --- a/spec/javascripts/app/models/post/interacations_spec.js +++ b/spec/javascripts/app/models/post/interacations_spec.js @@ -40,6 +40,13 @@ describe("app.models.Post.Interactions", function(){ jasmine.Ajax.requests.mostRecent().respondWith(ajaxSuccess); expect(this.post.get("participation")).toBeTruthy(); }); + + it("triggers a change on the likes collection", function() { + spyOn(this.interactions.likes, "trigger"); + this.interactions.like(); + jasmine.Ajax.requests.mostRecent().respondWith(ajaxSuccess); + expect(this.interactions.likes.trigger).toHaveBeenCalledWith("change"); + }); }); describe("unlike", function(){ @@ -56,7 +63,7 @@ describe("app.models.Post.Interactions", function(){ this.reshare = this.interactions.post.reshare(); }); - it("triggers a change on the model", function() { + it("triggers a change on the interactions model", function() { spyOn(this.interactions, "trigger"); this.interactions.reshare(); @@ -65,6 +72,13 @@ describe("app.models.Post.Interactions", function(){ expect(this.interactions.trigger).toHaveBeenCalledWith("change"); }); + it("triggers a change on the reshares collection", function() { + spyOn(this.interactions.reshares, "trigger"); + this.interactions.reshare(); + jasmine.Ajax.requests.mostRecent().respondWith(ajaxSuccess); + expect(this.interactions.reshares.trigger).toHaveBeenCalledWith("change"); + }); + it("adds the reshare to the default, activity and aspects stream", function() { app.stream = { addNow: $.noop }; spyOn(app.stream, "addNow"); diff --git a/spec/javascripts/app/views/likes_info_view_spec.js b/spec/javascripts/app/views/likes_info_view_spec.js index f0848675d..6166bfc82 100644 --- a/spec/javascripts/app/views/likes_info_view_spec.js +++ b/spec/javascripts/app/views/likes_info_view_spec.js @@ -21,19 +21,23 @@ describe("app.views.LikesInfo", function(){ it("fires on a model change", function(){ spyOn(this.view, "postRenderTemplate"); - this.view.model.interactions.trigger('change'); + this.view.model.interactions.likes.trigger("change"); expect(this.view.postRenderTemplate).toHaveBeenCalled(); }); }); describe("showAvatars", function(){ - beforeEach(function(){ - spyOn(this.post.interactions, "fetch").and.callThrough(); + it("calls fetch on the model's like collection", function() { + spyOn(this.post.interactions.likes, "fetch").and.callThrough(); + this.view.showAvatars(); + expect(this.post.interactions.likes.fetch).toHaveBeenCalled(); }); - it("calls fetch on the model's like collection", function(){ + it("triggers 'change' on the likes collection", function() { + spyOn(this.post.interactions.likes, "trigger"); this.view.showAvatars(); - expect(this.post.interactions.fetch).toHaveBeenCalled(); + jasmine.Ajax.requests.mostRecent().respondWith({status: 200, responseText: "{\"id\": 1}"}); + expect(this.post.interactions.likes.trigger).toHaveBeenCalledWith("change"); }); it("sets 'displayAvatars' to true", function(){ diff --git a/spec/javascripts/app/views/reshares_info_view_spec.js b/spec/javascripts/app/views/reshares_info_view_spec.js index b36853cc0..f5e6bf0db 100644 --- a/spec/javascripts/app/views/reshares_info_view_spec.js +++ b/spec/javascripts/app/views/reshares_info_view_spec.js @@ -22,19 +22,23 @@ describe("app.views.ResharesInfo", function(){ it("fires on a model change", function(){ spyOn(this.view, "postRenderTemplate"); - this.view.model.interactions.trigger("change"); + this.view.model.interactions.reshares.trigger("change"); expect(this.view.postRenderTemplate).toHaveBeenCalled(); }); }); describe("showAvatars", function(){ - beforeEach(function(){ - spyOn(this.post.interactions, "fetch").and.callThrough(); + it("calls fetch on the model's reshare collection", function() { + spyOn(this.post.interactions.reshares, "fetch").and.callThrough(); + this.view.showAvatars(); + expect(this.post.interactions.reshares.fetch).toHaveBeenCalled(); }); - it("calls fetch on the model's reshare collection", function(){ + it("triggers 'change' on the reshares collection", function() { + spyOn(this.post.interactions.reshares, "trigger"); this.view.showAvatars(); - expect(this.post.interactions.fetch).toHaveBeenCalled(); + jasmine.Ajax.requests.mostRecent().respondWith({status: 200, responseText: "{\"id\": 1}"}); + expect(this.post.interactions.reshares.trigger).toHaveBeenCalledWith("change"); }); it("sets 'displayAvatars' to true", function(){ From 951149dd24e498130075af27ecc1010561f1fa57 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Wed, 2 Nov 2016 16:19:31 +0100 Subject: [PATCH 08/71] Properly format JSON for reshares to hide 'reshare' button on already reshared posts closes #7169 fixes #4816 and #6594 --- Changelog.md | 1 + app/presenters/post_presenter.rb | 2 +- features/desktop/reshare.feature | 24 +++++++++++++++++++----- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Changelog.md b/Changelog.md index b6fd95c33..7a00e5461 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,7 @@ ## Bug fixes * Fix fetching comments after fetching likes [#7167](https://github.com/diaspora/diaspora/pull/7167) +* Hide 'reshare' button on already reshared posts [#7169](https://github.com/diaspora/diaspora/pull/7169) ## Features diff --git a/app/presenters/post_presenter.rb b/app/presenters/post_presenter.rb index ba1609c8a..86064fbf9 100644 --- a/app/presenters/post_presenter.rb +++ b/app/presenters/post_presenter.rb @@ -104,7 +104,7 @@ class PostPresenter < BasePresenter end def user_reshare - @post.reshare_for(current_user) + @post.reshare_for(current_user).try(:as_api_response, :backbone) end def already_participated_in_poll diff --git a/features/desktop/reshare.feature b/features/desktop/reshare.feature index 9c56da684..6afe52fb0 100644 --- a/features/desktop/reshare.feature +++ b/features/desktop/reshare.feature @@ -42,11 +42,25 @@ Feature: public repost And I sign in as "bob@bob.bob" Then I should see "Original post deleted by author" within ".reshare" - # should be covered in rspec, so testing that the post is added to - # app.stream in jasmine should be enough coverage - Scenario: When I reshare, it shows up on my profile page - Given I sign in as "alice@alice.alice" - And I confirm the alert after I follow "Reshare" + Scenario: Reshare a post from the stream + When I sign in as "alice@alice.alice" + Then I should see a ".reshare" within ".feedback" + When I confirm the alert after I follow "Reshare" Then I should see a flash message indicating success And I should see a flash message containing "successfully" And I should not see a ".reshare" within ".feedback" + + Scenario: Reshare a post from another user's profile + When I sign in as "alice@alice.alice" + And I am on "bob@bob.bob"'s page + Then I should see a ".reshare" within ".feedback" + When I confirm the alert after I follow "Reshare" + Then I should see a flash message indicating success + And I should see a flash message containing "successfully" + And I should not see a ".reshare" within ".feedback" + + Scenario: Try to reshare an already reshared post from another user's profile + Given the post with text "reshare this!" is reshared by "alice@alice.alice" + When I sign in as "alice@alice.alice" + And I am on "bob@bob.bob"'s page + Then I should not see a ".reshare" within ".feedback" From a73e1baaeded9f888a5955a850fce19d786df8d3 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Wed, 2 Nov 2016 23:44:20 +0100 Subject: [PATCH 09/71] Show spinner when loading comments closes #7170 --- Changelog.md | 1 + .../javascripts/app/views/comment_stream_view.js | 2 ++ app/assets/stylesheets/comments.scss | 16 +++++++++++++++- app/assets/templates/comment-stream_tpl.jst.hbs | 8 ++++++++ .../app/views/comment_stream_view_spec.js | 13 +++++++++++++ 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 7a00e5461..bfe47c7e8 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,7 @@ * Hide 'reshare' button on already reshared posts [#7169](https://github.com/diaspora/diaspora/pull/7169) ## Features +* Show spinner when loading comments in the stream [#7170](https://github.com/diaspora/diaspora/pull/7170) # 0.6.1.0 diff --git a/app/assets/javascripts/app/views/comment_stream_view.js b/app/assets/javascripts/app/views/comment_stream_view.js index 0119b3329..48df8afad 100644 --- a/app/assets/javascripts/app/views/comment_stream_view.js +++ b/app/assets/javascripts/app/views/comment_stream_view.js @@ -104,10 +104,12 @@ app.views.CommentStream = app.views.Base.extend({ }, expandComments: function(evt){ + this.$(".loading-comments").removeClass("hidden"); if(evt){ evt.preventDefault(); } this.model.comments.fetch({ success: function() { this.$("div.comment.show_comments").addClass("hidden"); + this.$(".loading-comments").addClass("hidden"); }.bind(this) }); } diff --git a/app/assets/stylesheets/comments.scss b/app/assets/stylesheets/comments.scss index 3a9828c8f..7b6d76757 100644 --- a/app/assets/stylesheets/comments.scss +++ b/app/assets/stylesheets/comments.scss @@ -1,13 +1,27 @@ .comment_stream { .show_comments { - margin-top: 5px; border-top: 1px solid $border-grey; + line-height: $line-height-computed; + margin-top: 5px; a { color: $text-grey; font-size: 13px; } .media { margin-top: 10px; } } + + .loading-comments { + height: $line-height-computed + 11px; // height of .show_comments: line-height, 10px margin, 1px border + margin-top: -$line-height-computed - 11px; + + .loader { + height: 20px; + width: 20px; + } + + .media { margin: 5px; } + } + .comments > .comment, .comment.new-comment-form-wrapper { .avatar { diff --git a/app/assets/templates/comment-stream_tpl.jst.hbs b/app/assets/templates/comment-stream_tpl.jst.hbs index 0b6678a10..e7343507d 100644 --- a/app/assets/templates/comment-stream_tpl.jst.hbs +++ b/app/assets/templates/comment-stream_tpl.jst.hbs @@ -6,6 +6,14 @@ + +
{{#if loggedIn}} diff --git a/spec/javascripts/app/views/comment_stream_view_spec.js b/spec/javascripts/app/views/comment_stream_view_spec.js index 0317f7f0b..104cb3c7a 100644 --- a/spec/javascripts/app/views/comment_stream_view_spec.js +++ b/spec/javascripts/app/views/comment_stream_view_spec.js @@ -164,6 +164,19 @@ describe("app.views.CommentStream", function(){ }).join("") ); }); + + it("shows the spinner when loading comments and removes it on success", function() { + this.view.render(); + expect(this.view.$(".loading-comments")).toHaveClass("hidden"); + + this.view.expandComments(); + expect(this.view.$(".loading-comments")).not.toHaveClass("hidden"); + + jasmine.Ajax.requests.mostRecent().respondWith({ + status: 200, responseText: JSON.stringify([]) + }); + expect(this.view.$(".loading-comments")).toHaveClass("hidden"); + }); }); describe("pressing a key when typing on the new comment box", function(){ From 260cc233575b8ce02fb98d60157042e175bde8ab Mon Sep 17 00:00:00 2001 From: Dennis Schubert Date: Sun, 6 Nov 2016 02:28:30 +0100 Subject: [PATCH 10/71] updated 29 locale files [ci skip] --- config/locales/devise/devise.cs.yml | 8 + config/locales/devise/devise.da.yml | 44 +- config/locales/devise/devise.fr.yml | 4 +- config/locales/devise/devise.hy.yml | 8 +- config/locales/devise/devise.ja.yml | 10 +- config/locales/devise/devise.oc.yml | 107 ++ config/locales/devise/devise.sv.yml | 25 +- config/locales/devise/devise.te.yml | 10 +- config/locales/diaspora/cs.yml | 62 ++ config/locales/diaspora/da.yml | 10 +- config/locales/diaspora/de-CH.yml | 534 +++++++++ config/locales/diaspora/de.yml | 6 +- config/locales/diaspora/eo.yml | 13 +- config/locales/diaspora/fr.yml | 20 +- config/locales/diaspora/hy.yml | 25 +- config/locales/diaspora/ia.yml | 16 +- config/locales/diaspora/ja.yml | 192 ++-- config/locales/diaspora/oc.yml | 1098 +++++++++++++++++++ config/locales/diaspora/ru.yml | 10 +- config/locales/diaspora/te.yml | 14 +- config/locales/javascript/javascript.cs.yml | 75 ++ config/locales/javascript/javascript.da.yml | 6 + config/locales/javascript/javascript.hy.yml | 28 + config/locales/javascript/javascript.ia.yml | 2 +- config/locales/javascript/javascript.ja.yml | 34 +- config/locales/javascript/javascript.nb.yml | 28 + config/locales/javascript/javascript.oc.yml | 320 ++++++ config/locales/javascript/javascript.sv.yml | 13 + config/locales/javascript/javascript.te.yml | 17 +- 29 files changed, 2541 insertions(+), 198 deletions(-) create mode 100644 config/locales/devise/devise.oc.yml create mode 100644 config/locales/diaspora/de-CH.yml create mode 100644 config/locales/diaspora/oc.yml create mode 100644 config/locales/javascript/javascript.oc.yml diff --git a/config/locales/devise/devise.cs.yml b/config/locales/devise/devise.cs.yml index f69a8529a..775bb4368 100644 --- a/config/locales/devise/devise.cs.yml +++ b/config/locales/devise/devise.cs.yml @@ -12,9 +12,11 @@ cs: resend_confirmation: "Zaslat instrukce o potvrzení" send_instructions: "Během několika minut obdržíte email s instrukcemi k potvrzení vašeho účtu." failure: + already_authenticated: "již jste přihlášen" inactive: "Váš účet ještě nebyl aktivován." invalid: "Neplatné uživatelské jméno nebo heslo." invalid_token: "Neplatný autentizační token." + last_attempt: "Máte ještě jeden pokus před uzamčením účtu" locked: "Váš účet je uzamčen." not_found_in_database: "Chybný email nebo heslo." timeout: "Vaše sezení vypršelo, pro pokračování se prosím přihlašte znovu." @@ -34,6 +36,8 @@ cs: accept_at: "na %{url}, přijmout pozvání můžete pomocí odkazu níže." has_invited_you: "%{name}" have_invited_you: "%{names} vás pozvali, abyste vstoupili do Diaspory" + password_change: + subject: "Heslo změněno" reset_password_instructions: change: "Změnit moje heslo" ignore: "Pokud nechcete potvrdit pozvání, tento e-mail prosím ignorujte." @@ -64,6 +68,7 @@ cs: signed_up: "Úspěšně jste vytvořili účet. Pokud je tak server nastaven, byl vám zaslán potvrzovací email." updated: "Úspěšně jste upravili svůj účet." sessions: + already_signed_out: "Úspěšně odhlášen." new: login: "Přihlásit" modern_browsers: "podporuje pouze moderní prohlížeče." @@ -85,9 +90,12 @@ cs: new: resend_unlock: "Znovu zaslat instrukce k odemknutí účtu." send_instructions: "Během několika minut obdržíte email s instrukcemi k odemknutí vašeho účtu." + send_paranoid_instructions: "Váš účet již existuje, během několika minut dostanete email s instrukcemi, jak je odemknout" unlocked: "Váš účet byl úspěšně odemknut. Nyní jste přihlášeni." errors: messages: already_confirmed: "již bylo potvrzeno" + confirmation_period_expired: "nutné potvrdit do %{period}, prosím, požádejte znovu" + expired: "vypršel, prosím požadujte nový" not_found: "nenalezeno" not_locked: "nebylo zamčeno" \ No newline at end of file diff --git a/config/locales/devise/devise.da.yml b/config/locales/devise/devise.da.yml index 645d41690..8243f6d7d 100644 --- a/config/locales/devise/devise.da.yml +++ b/config/locales/devise/devise.da.yml @@ -7,17 +7,20 @@ da: devise: confirmations: - confirmed: "Din konto er blevet bekræftet. Du er nu logget ind" + confirmed: "Din e-mailadresse er blevet bekræftet." new: resend_confirmation: "Send bekræftelse igen" - send_instructions: "Du vil modtage en e-mail med instruktioner om hvordan du bekræfter din konto om et par minutter." + send_instructions: "Du vil modtage en e-mail med instruktioner om hvordan du bekræfter din e-mailadresse om et øjeblik." + send_paranoid_instructions: "Hvis din e-mailadresse eksisterer i vores database, vil du modtage en e-mail med instruktioner om hvordan du bekræfter din e-mailadresse om et øjeblik." failure: - inactive: "Din konto er endnu ikke aktiveret." + already_authenticated: "Du er allerede logget ind." + inactive: "Din konto er ikke aktiveret endnu." invalid: "Forkert %{authentication_keys} eller adgangskode." invalid_token: "Ugyldig autentifikation." + last_attempt: "Du har et forsøg mere inden din konto bliver låst." locked: "Din konto er låst." - not_found_in_database: "Forkert e-mail eller adgangskode." - timeout: "Du har været inaktiv for længe. Log ind for at fortsætte." + not_found_in_database: "Ugyldig %{authentication_keys} eller adgangskode." + timeout: "Du har været inaktiv for længe. Log ind igen for at fortsætte." unauthenticated: "Du skal logge ind eller oprette en konto for at fortsætte." unconfirmed: "Du skal bekræfte din e-mailkonto før du fortsætter." invitations: @@ -34,6 +37,8 @@ da: accept_at: "på %{url}, du kan acceptere ved at følge linket nedenunder." has_invited_you: "%{name}" have_invited_you: "%{names} har inviteret dig til at deltage i Diaspora." + password_change: + subject: "Kodeord er blevet ændret" reset_password_instructions: change: "Skift min adgangskode" ignore: "Hvis du ikke har anmodet om dette, bedes du ignorere denne e-mail." @@ -46,6 +51,9 @@ da: subject: "Aktiver instruktioner" unlock: "Aktiver min konto" welcome: "Velkommen %{email}!" + omniauth_callbacks: + failure: "Kunne ikke godkende dig fra %{kind} fordi \"%{reason}\"." + success: "Godkendelse modtaget fra %{kind} kontoen." passwords: edit: change_password: "skift adgangskode" @@ -57,13 +65,21 @@ da: no_account: "Ingen konto med denne email adresse eksisterer." reset_password: "Nulstil adgangskode" send_password_instructions: "Send mig instruktioner til nulstilning af adgangskode" - send_instructions: "Du vil modtage en e-mail med instruktioner om hvordan du kan nulstille din adgangskode om et par minutter." - updated: "Dit adgangskode er nu ændret. Du er nu logget ind." + no_token: "Du kan ikke tilgå denne side uden at komme fra en e-mail med nulstilling af dit kodeord. Hvis du kommer fra sådan en e-mail, undersøg venligst at du har brugt hele den URL der stod i mailen." + send_instructions: "Du vil modtage en e-mail med instruktioner om hvordan du kan nulstille din adgangskode om et øjeblik." + send_paranoid_instructions: "Hvis din e-mailadresse eksisterer i vores database, vil du modtage et link med en kodeords-nulstilling i din mailboks om et øjeblik." + updated: "Dit adgangskode er blevet ændret. Du er nu logget ind." + updated_not_active: "Dit kodeord er nu blevet ændret." registrations: destroyed: "Farvel! Din konto er nu lukket. Vi håber at se dig igen snart." signed_up: "Du har nu tilmeldt dig. En bekræftelse er blevet sendt til din e-mail, hvis den er aktiveret." - updated: "Din konto er nu opdateret." + signed_up_but_inactive: "Din registrering er godkendt, men vi kunne ikke logge dig ind da din konto endnu ikke er blevet aktiveret." + signed_up_but_locked: "Din registrering er godkendt, men vi kunne ikke logge dig ind da din konto er låst." + signed_up_but_unconfirmed: "Der er blevet sendt en besked til din e-mailadresse med et bekræftelses-link. Følg venligst dette link for at aktivere din konto." + update_needs_confirmation: "Du har nu opdateret din konto, men vi er nødt bekræfte din nye e-mailadresse. Check din e-mail og følg linket for at bekræfte din nye e-mailadresse." + updated: "Din konto er blevet opdateret." sessions: + already_signed_out: "Du er nu logget ud." new: login: "Log ind" modern_browsers: "understøtter kun nyere browsere." @@ -84,10 +100,16 @@ da: unlocks: new: resend_unlock: "Send vejledningen til aktivering af konto igen" - send_instructions: "Du vil modtage en e-mail om et par minutter med instruktioner om hvordan du kan låse din konto op." - unlocked: "Din konto er blevet låst op. Du er nu logget ind." + send_instructions: "Du vil modtage en e-mail om et øjeblik med instruktioner om hvordan du kan låse din konto op." + send_paranoid_instructions: "Hvis din konto eksisterer, vil du modtage en e-mail med instruktioner om hvordan du læser den op om et øjeblik." + unlocked: "Din konto er blevet låst op. Log ind for at fortsætte." errors: messages: already_confirmed: "var allerede bekræftet - prøv at logge ind" + confirmation_period_expired: "skal bekræftes indenfor %{period}, bed venligst om en ny" + expired: "er udløbet, bed venligst om en ny" not_found: "ikke fundet" - not_locked: "blev ikke låst" \ No newline at end of file + not_locked: "blev ikke låst" + not_saved: + one: "En fejl forhindrede denne %{resource} i at blive gemt:" + other: "%{count} fejl forhindrede denne %{resource} i at blive gemt:" \ No newline at end of file diff --git a/config/locales/devise/devise.fr.yml b/config/locales/devise/devise.fr.yml index a6dd4277f..411852a96 100644 --- a/config/locales/devise/devise.fr.yml +++ b/config/locales/devise/devise.fr.yml @@ -15,11 +15,11 @@ fr: failure: already_authenticated: "Vous êtes déjà connecté-e" inactive: "Votre compte n’a pas encore été activé." - invalid: "Pseudo ou mot de passe invalide." + invalid: "%{authentication_keys} ou mot de passe invalide." invalid_token: "Jeton d’authentification invalide." last_attempt: "Vous avez encore une tentative avant que votre compte ne soit verrouillé." locked: "Votre compte est verrouillé." - not_found_in_database: "Courriel ou mot de passe invalide." + not_found_in_database: "%{authentication_keys} ou mot de passe invalide." timeout: "Votre session a expiré. Veuillez vous connecter de nouveau pour continuer." unauthenticated: "Vous devez vous connecter ou vous inscrire avant de continuer." unconfirmed: "Vous devez confirmer votre compte avant de continuer." diff --git a/config/locales/devise/devise.hy.yml b/config/locales/devise/devise.hy.yml index f3213a5d3..7fdc8d1f3 100644 --- a/config/locales/devise/devise.hy.yml +++ b/config/locales/devise/devise.hy.yml @@ -13,6 +13,7 @@ hy: send_instructions: "Րոպեների ընթացքում նամակ կստանաս՝ էլ․հասցեդ հաստատելու ցուցումներով։" send_paranoid_instructions: "Եթե քո էլ․հասցեն կա մեր տվյալների բազայում, րոպեների ընթացքում նամակ կստանաս՝ էլ․հասցեդ հաստատելու ցուցումներով։" failure: + already_authenticated: "Արդեն մուտք գործած ես։" inactive: "Քո հաշիվը դեռ ակտիվացված չէ։" invalid: "Սխալ %{authentication_keys} կամ գաղտնաբառ։" invalid_token: "Նույնականացման սխալ։" @@ -99,5 +100,10 @@ hy: errors: messages: already_confirmed: "արդեն հաստատվել է, փորձիր մուտք գործել" + confirmation_period_expired: "պետք է հաստատվի %{period}վա ընթացքում, խնդրում ենք նորը հայցիր" + expired: "լրացել է, խնդրում ենք նորը հայցիր" not_found: "չի գտնվել" - not_locked: "կողպված չէ" \ No newline at end of file + not_locked: "արգելափակված չէր" + not_saved: + one: "1 սխալանք խոչընդոտեց այս %{resource}֊ի պահպանվելուն․" + other: "%{count} սխալանք խոչընդոտեցին այս %{resource}֊ի պահպանվելուն․" \ No newline at end of file diff --git a/config/locales/devise/devise.ja.yml b/config/locales/devise/devise.ja.yml index 7cfff0e10..4f2f0a0fd 100644 --- a/config/locales/devise/devise.ja.yml +++ b/config/locales/devise/devise.ja.yml @@ -15,11 +15,11 @@ ja: failure: already_authenticated: "既にサインイン済みです。" inactive: "アカウントはまだ有効になっていません。" - invalid: "ユーザ名またはパスワードが不正です。" + invalid: "%{authentication_keys} またはパスワードが不正です。" invalid_token: "無効な認証トークンです。" last_attempt: "アカウントがロックされるまでに、まだ複数回試すことができます。" locked: "アカウントがロックされています。" - not_found_in_database: "メールアドレスまたはパスワードが無効です。" + not_found_in_database: "%{authentication_keys}またはパスワードが無効です。" timeout: "セッション切れになりました。続くにはもう一度サインインしてください。" unauthenticated: "進むにはサインインまたは新規登録する必要があります。" unconfirmed: "進にはアカウントを確認する必要があります。" @@ -61,7 +61,7 @@ ja: new_password: "新しいパスワード" new: email: "メールアドレス" - forgot_password: "パスワードを忘れましたか。" + forgot_password: "パスワードを忘れましたか?" no_account: "このメールアドレスに一致するアカウントは存在しません。招待待ちの方は、なるべく早く出せるように努力していますので、もう少々お待ちください。" reset_password: "パスワードの再設定" send_password_instructions: "パスワード再発行の手続きメールを送ってください。" @@ -86,12 +86,12 @@ ja: password: "パスワード" remember_me: "ログインしたままにする" sign_in: "サインイン" - username: "ユーザ名" + username: "ユーザー名" signed_in: "サインインに成功しました。" signed_out: "サインアウトに成功しました。" shared: links: - forgot_your_password: "パスワードを忘れましたか。" + forgot_your_password: "パスワードを忘れましたか?" receive_confirmation: "認証手続きメールが届きませんでしたか。" receive_unlock: "ロック解除の説明が届きませんでしたか。" sign_in: "サインイン" diff --git a/config/locales/devise/devise.oc.yml b/config/locales/devise/devise.oc.yml new file mode 100644 index 000000000..2c88afc6f --- /dev/null +++ b/config/locales/devise/devise.oc.yml @@ -0,0 +1,107 @@ +# Copyright (c) 2010-2013, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + + + +oc: + devise: + confirmations: + confirmed: "Vòstre compte es estat confirmat. Ara, sètz connectat." + new: + resend_confirmation: "Renviar las instruccions de confirmacion" + failure: + already_authenticated: "Sètz ja connectat" + inactive: "Vòstre compte es pas encara estat activat." + invalid: "%{authentication_keys} o senhal invalid." + invalid_token: "Geton d'autentificacion invalid." + last_attempt: "Avètz encara una tempatativa abans que vòstre compte siá verrolhat." + locked: "Vòstre compte es verrolhat." + not_found_in_database: "%{authentication_keys} o senhal pas valid." + timeout: "Vòstra session a expirat. Connectatz-vos tornamai per contunhar." + unauthenticated: "Vos cal vos connectar o vos inscriure abans de contunhar." + unconfirmed: "Vos cal confirmar vòstre compte abans de contunhar." + invitations: + invitation_token_invalid: "O planhèm ! Aqueste convit es pas valid." + send_instructions: "Vòstre convit es estat mandat." + updated: "Vòstre senhal es estat creat. Ara, sètz connectat." + mailer: + confirmation_instructions: + confirm: "Confirmar mon compte" + subject: "Instruccions de confirmacion" + you_can_confirm: "Podètz confirmar vòstre compte en clicant sul ligam çaijós :" + hello: "Adieusiatz %{email} !" + inviter: + accept_at: "a %{url}, lo podètz acceptar a travèrs lo ligal çaijós." + has_invited_you: "%{name}" + have_invited_you: "%{names} vos a convidat a rejónher diaspora*" + password_change: + subject: "Senhal cambiat" + reset_password_instructions: + change: "Cambiar mon senhal" + ignore: "Se avètz pas demandat aquò, mercé d'ignorar aqueste corrièl." + subject: "Instruccions de reïnicializacion del senhal" + wont_change: "Vòstre senhal cambiarà pas tant que n'auretz pas creat un novèl en accedissent al ligam çaijós." + unlock_instructions: + account_locked: "Vòstre compte es estat verrolhat en rason d'un nombre excessiu de temptativas infructuosas de connexion." + click_to_unlock: "Clicatz sul ligam çaijós per desblocar vòstre compte :" + subject: "Instruccions de desverrolhatge" + unlock: "Desverrolhar mon compte" + welcome: "Benvenguda %{email} !" + omniauth_callbacks: + failure: "Impossible de vos indentificar dempuèi %{kind} perque %{reason}" + success: "Autentificacion capitada dempuèi lo compte %{kind}." + passwords: + edit: + change_password: "Cambiar mon senhal" + confirm_password: "Confirmar la senhal" + new_password: "Senhal novèl" + new: + email: "Adreça de corrièl" + forgot_password: "Avètz doblidat vòstre senhal ?" + no_account: "Cap de compte es pas associat a aquesta adreça de corrièr electronic." + reset_password: "Reïnicializar lo senhal" + send_password_instructions: "Mandar las instruccions de reïnicializacion de senhal" + updated: "Vòstre senhal es estat modificat. Ara, sètz connectat." + updated_not_active: "Vòstre senhal es estat cambiat amb succès." + registrations: + signed_up: "Vòstra inscripcion es estada efectuada. Se aquesta darrièra es activada, una confirmacion es estada mandada a vòstra adreça de corrièr electronic." + signed_up_but_inactive: "Vos sètz inscrit amb succès. Pasmens, vos podètz pas connectar perque vòstre compte es pas encara activat." + signed_up_but_locked: "Vos sètz inscrit amb succès. Pasmens, podètz pas vos connectar perque vòstre compte es verrolhat." + signed_up_but_unconfirmed: "Un corrièr electronic amb un ligam de confirmacion vos es estat mandat. Seguissètz aqueste ligam per activar vòstre compte." + update_needs_confirmation: "Avètz mes a jorn corrèctament vòstre compte mas nos cal verificar vòstra novèla adreça electronica. Consultatz vòstra bóstia de letras e clicatz sul ligam de confirmacion." + updated: "Avètz mes a jorn vòstre compte." + sessions: + already_signed_out: "Desconnexion capitada." + new: + login: "Connexion" + modern_browsers: "supòrta solament de navigadors modèrnes." + password: "Senhal" + remember_me: "Se remembrar de ieu" + sign_in: "Connexion" + username: "Nom d'utilizaire" + signed_in: "Ara, sètz connectat." + signed_out: "Ara, sètz desconnectat." + shared: + links: + forgot_your_password: "Avètz doblidat vòstre senhal ?" + receive_confirmation: "Avètz pas recebut las instruccions de confirmacion ?" + receive_unlock: "Avètz pas recebut las instruccions de desblocatge ?" + sign_in: "Connexion" + sign_up: "Crear un compte" + sign_up_closed: "Las inscripcions son tampadas pel moment." + unlocks: + new: + resend_unlock: "Renviar las instruccions de desblocatge" + send_paranoid_instructions: "Se vòstre compte existís, recebretz un corrièl amb d'instruccions per lo desverrolhar d'aicí qualques minutas." + unlocked: "Vòstre compte es estat verrolhat. Ara, sètz connectat." + errors: + messages: + already_confirmed: "es ja estat confirmat" + confirmation_period_expired: "auriá degut èsser confirmat dins los %{period}, demandatz un novèl" + expired: "a expirat, fasètz una novèla demanda" + not_found: "pas trobat" + not_locked: "èra pas verrolhat" + not_saved: + one: "1 error a empachat aqueste %{resource} d'èsser utilizat" + other: "%{count} errors an empachat aqueste %{resource} d'èsser utilizat" \ No newline at end of file diff --git a/config/locales/devise/devise.sv.yml b/config/locales/devise/devise.sv.yml index 37360dc31..765609003 100644 --- a/config/locales/devise/devise.sv.yml +++ b/config/locales/devise/devise.sv.yml @@ -7,19 +7,19 @@ sv: devise: confirmations: - confirmed: "Ditt konto har verifierats och du är inloggad." + confirmed: "Din e-postadress har verifierats." new: resend_confirmation: "Skicka instruktioner för att bekräfta kontot igen" - send_instructions: "Du kommer att få ett e-brev med instruktioner för att verifiera ditt konto inom några minuter." + send_instructions: "Du kommer att få ett e-brev med instruktioner för att verifiera din e-postadress inom några minuter." failure: - inactive: "Ditt konto är inte aktiverat." - invalid: "Fel användarnamn eller lösenord." + inactive: "Ditt konto är inte aktiverat ännu." + invalid: "Fel %{authentication_keys} eller lösenord." invalid_token: "Ogiltig identifiering." locked: "Ditt konto är låst." - not_found_in_database: "Ogiltig e-postadress eller ogiltigt lösenord." - timeout: "Din session är avslutad, var vänlig logga in igen." + not_found_in_database: "Ogiltig %{authentication_keys} eller ogiltigt lösenord." + timeout: "Din session är avslutad, var vänlig logga in igen för att fortsätta." unauthenticated: "Du måste logga in innan du kan fortsätta." - unconfirmed: "Du måste verifiera ditt konto innan du kan fortsätta." + unconfirmed: "Du måste verifiera din e-postadress innan du kan fortsätta." invitations: invitation_token_invalid: "Vi ber om ursäkt! Den här inbjudan är inte giltig!" send_instructions: "Din inbjudan är nu skickad." @@ -58,7 +58,7 @@ sv: reset_password: "Återställ lösenord" send_password_instructions: "Skicka information om hur jag återställer lösenordet" send_instructions: "Du kommer att få ett e-brev med instruktioner för att återställa lösenordet inom några minuter." - updated: "Ditt lösenord har ändrats och du är inloggad." + updated: "Ditt lösenord har ändrats och du är nu inloggad." registrations: destroyed: "Ditt konto är avslutat. På återseende!" signed_up: "Du har skapat ett konto. Beroende på dina inställningar kan en bekräftelse ha skickats till din e-postadress." @@ -85,9 +85,12 @@ sv: new: resend_unlock: "Skicka upplåsningsinstruktioner igen" send_instructions: "Du kommer att få ett e-brev med instruktioner för att låsa upp ditt konto inom några minuter." - unlocked: "Ditt konto har är nu upplåst och du är inloggad" + unlocked: "Ditt konto har är nu upplåst. Logga in för att fortsätta." errors: messages: - already_confirmed: "var redan bekräftad" + already_confirmed: "var redan bekräftad, prova att logga in" not_found: "inte funnet" - not_locked: "var ej låst" \ No newline at end of file + not_locked: "var ej låst" + not_saved: + one: "1 fel hindrade denna %{resource} från att sparas:" + other: "%{count} fel hindrade denna %{resource} från att sparas:" \ No newline at end of file diff --git a/config/locales/devise/devise.te.yml b/config/locales/devise/devise.te.yml index 9ef5ada07..86209d875 100644 --- a/config/locales/devise/devise.te.yml +++ b/config/locales/devise/devise.te.yml @@ -7,19 +7,19 @@ te: devise: confirmations: - confirmed: "మీ ఖాతా విజయవంతంగా ధృవీకరించబడింది. ఇప్పుడు మీరు ప్రవేశించారు." + confirmed: "మీ ఖాతా విజయవంతంగా ధృవీకరించబడింది." new: resend_confirmation: "నిర్ధారణ సూచనలను మళ్ళీ పంపించు" send_instructions: "మీరు కొన్ని నిమిషాల్లో మీ ఖాతాను నిర్ధారించడానికి అవసరమైన సూచనలతో కూడిన ఇమెయిల్ను అందుకుంటారు." failure: inactive: "మీ ఖాతా ఇంకా చేతనం కాలేదు." - invalid: "చెల్లని వాడుకరి పేరు లేదా సంకేతపదం." + invalid: "చెల్లని %{authentication_keys} లేదా సంకేతపదం." invalid_token: "చెల్లని ప్రమాణీకరణ టోకెన్." locked: "మీ ఖాతా లాక్ చెయ్యబడింది." - not_found_in_database: "చెల్లని ఈమెయిలు లేదా సంకేతపదం." + not_found_in_database: "చెల్లని %{authentication_keys} లేదా సంకేతపదం." timeout: "మీ సెషన్ గడువు ముగిసింది, కొనసాగించటానికి మళ్ళీ ప్రవేశించండి." unauthenticated: "మీరు కొనసాగే ముందు లోనికి ప్రవేశించాలి లేదా నమోదుచేసుకోండి." - unconfirmed: "మీరు కొనసాగించే ముందు మీ ఖాతాను నిర్ధారించండి." + unconfirmed: "మీరు కొనసాగించే ముందు మీ ఈమెయిలు చిరునామాను నిర్ధారించాలి." invitations: invitation_token_invalid: "అందించబడిన ఆహ్వానాన్ని టోకెన్ చెల్లదు!" send_instructions: "మీ ఆహ్వానాన్ని పంపించాం." @@ -88,6 +88,6 @@ te: unlocked: "మీ ఖాతా విజయవంతంగా అన్లాక్ చేయబడింది. ఇప్పుడు మీరు సైన్ ఇన్ అయ్యారు." errors: messages: - already_confirmed: "ఇప్పటికే నిర్ధారించబడింది" + already_confirmed: "ఇప్పటికే నిర్ధారించబడింది, దయచేసి మళ్ళీ ప్రవేశించ ప్రయత్నించండి." not_found: "దొరకలేదు" not_locked: "లాక్ కాలేదు" \ No newline at end of file diff --git a/config/locales/diaspora/cs.yml b/config/locales/diaspora/cs.yml index 1447a20cc..70f712697 100644 --- a/config/locales/diaspora/cs.yml +++ b/config/locales/diaspora/cs.yml @@ -118,6 +118,7 @@ cs: are_you_sure_unlock_account: "Určitě chcete odemknout teto účet ?" close_account: "zrušit účet" email_to: "E-mailová adresa, kterou chcete pozvat" + invite: "Pozvat" lock_account: "Zamknout Účet" under_13: "Zobrazit uživatele mladší 13 let (COPPA)" unlock_account: "Odemknout účet" @@ -149,13 +150,41 @@ cs: access: "%{name} potřebný přístup do" approve: "Schválit" bad_request: "Chybějící ID nebo URI" + client_id_not_found: "Nebyl nalezen klient s client_id rovným %{client_id} s přesměrováním na URI %{redirect_uri}" deny: "zamítnout" no_requirement: "%{name} Nevyžaduje oprávnění" redirection_message: "Opravdu chcete dát přístup %{redirect_uri}" + error_page: + contact_developer: "Můžete kontaktovat vývojáře aplikace a zahrnout následující detailní chybovou hlášku:" + could_not_authorize: "Aplikace nemůže být autorizována" + login_required: "Pro přístup k aplikaci musíte být autorizován přihlášením." + title: "Jejda! Něco se pokazilo :-(" + scopes: + name: + name: "jméno" + nickname: + name: "přezdívka" + openid: + description: "To umožňuje aplikaci přečíst váš základní profil." + name: "základní profil" + picture: + description: "To zajišťuje omezení práv aplikace k obrázku" + name: "Obrázek" + profile: + description: "To umožňuje aplikaci číst váš rozšířený profil" + name: "Rozšířený profil" + read: + name: "číst profil, stream a konverzace" + write: + name: "posílat příspěvky, konverzace a reakce" user_applications: index: + access: "%{name} má přístup k:" edit_applications: "Aplikace" + no_requirement: "%{name} nevyžaduje oprávnění" title: "Povolené aplikace" + no_applications: "Nemáte žádné autorizované aplikace" + revoke_autorization: "Odebrat oprávnění" are_you_sure: "Jste si jisti?" are_you_sure_delete_account: "Opravdu chcete uzavřít svůj účet? Tuto operaci nelze vrátit!" aspect_memberships: @@ -520,6 +549,20 @@ cs: tutorial: "tutoriál" tutorials: "tutoriály" wiki: "wiki" + home: + default: + be_who_you_want_to_be: "Buď, kým chceš být" + choose_your_audience: "Vyber si své publikum" + headline: "Vítejte v %{pod_name}" + own_your_data: "Vlastni svá data" + podmin: + admin_panel: "admin panel" + contact_irc: "kontaktujte nás na IRC" + contribute: "Pomoci" + create_an_account: "Vytvořit účet" + headline: "Vítej, příteli." + make_yourself_an_admin: "Vytvořte sebe jako administrátora" + update_your_pod_info: "Můžete najít %{update_instructions}." invitation_codes: not_valid: "Kód této pozvánky již není platný." invitations: @@ -809,11 +852,18 @@ cs: upload: "Nahrajte novou profilovou fotku!" show: show_original_post: "Zobrazit původní příspěvek" + polls: + votes: + few: "Zbývá %{count} hlasy" + one: "Zbývá %{count} hlas" + other: "Zbývá %{count} hlasů" + zero: "Zbývá %{count} hlasů" posts: presenter: title: "Příspěvek uživatele %{name}" show: forbidden: "Nemáte povolení k této akci." + location: "Odesláno z: %{location}" photos_by: few: "%{count} fotky uživatele %{author}" one: "Jedna fotka uživatele %{author}" @@ -825,11 +875,18 @@ cs: profiles: edit: allow_search: "Umožnit, aby vás lidé mohli najít na Diaspoře" + basic: "Můj základní profil" + basic_hint: "Každá položka vašeho profilu je volitelné. Váš základní profil bude vždy viditelný veřejně." + extended: "Můj rozšířený profil" + extended_visibility_text: "Viditelnost vašeho rozšířeného profilu" first_name: "Jméno" last_name: "Příjmení" + limited: "Omezený" nsfw_check: "Označit vše, co sdílím, jako citlivý obsah" nsfw_explanation: "NSFW ('citlivý obsah') je vnitřní standard diaspory* pro obsah, který nemusí být vhodný k prohlížení, když jste v práci. Pokud sdílíte takový obsah často, prosím zatrhněte tuto možnost, čímž budou všechny Vaše příspěvky skryty z uživatelských poudů, dokud se daný uživatel nerozhodne si je prohlédnout." nsfw_explanation2: "Pokud se rozhodnete tuto možnost nezatrhnout, přidávejte prosím štítek #nsfw pokaždé když sdílíte takový obsah." + public: "Veřejné" + settings: "Nastavení profilu" update_profile: "Aktualizovat profil" your_bio: "Něco o vás" your_birthday: "Vaše narozeniny" @@ -874,6 +931,7 @@ cs: post_label: "Příspěvek: %{title}" reason_label: "Důvod: %{text}" reported_label: "Oznámil/а %{person}" + reported_user_details: "Detaily reportujícího uživatele" review_link: "Označit jako zkontrolované" status: destroyed: "Příspěvek byl zničen" @@ -906,6 +964,7 @@ cs: really_disconnect: "odpojit %{service}?" services_explanation: "Připojování se k službám vám dá možnost na nich publikovat své příspěvky hned co je napíšete na diaspoře*." share_to: "Nasdílet %{provider}" + title: "Spravovat služby připojení" provider: facebook: "Facebook" tumblr: "Tumblr" @@ -1040,6 +1099,7 @@ cs: auto_follow_aspect: "Aspekt, do kterého zařadit automaticky sledované uživatele" auto_follow_back: "Automaticky sledovat toho, kdo sleduje vás" change: "Změnit" + change_color_theme: "Změnit barevné schéma" change_email: "Změnit e-mail" change_language: "Změnit jazyk" change_password: "Změnit heslo" @@ -1102,6 +1162,8 @@ cs: public: does_not_exist: "Uživatel %{username} neexistuje!" update: + color_theme_changed: "Barevné schéma úspěšně změněno" + color_theme_not_changed: "Při změně barevného schema nastala chyba" email_notifications_changed: "Oznámení e-mailem změněno" follow_settings_changed: "Následující nastavení se změnila" follow_settings_not_changed: "Následující nastavení se nepodařilo změnit" diff --git a/config/locales/diaspora/da.yml b/config/locales/diaspora/da.yml index 229b78e9e..83c15bb60 100644 --- a/config/locales/diaspora/da.yml +++ b/config/locales/diaspora/da.yml @@ -587,7 +587,7 @@ da: check_token: not_found: "Invitation ikke fundet" create: - empty: "Indsæt mindst en e-mailadresse." + empty: "Skriv mindst en e-mailadresse." no_more: "Du har ikke flere invitationer." note_already_sent: "Der er allerede blevet sendt en invitation til %{emails}" rejected: "Der var problemer med de følgende e-mail adresser: " @@ -949,7 +949,7 @@ da: invalid_invite: "Det invitations-link som du anvendte er ikke længere gyldigt!" new: email: "E-mail" - enter_email: "Indtast din e-mailadresse" + enter_email: "Skriv din e-mailadresse" enter_password: "Indtast en adgangskode (mindst seks tegn)" enter_password_again: "Indtast den samme adgangskode som før" enter_username: "Vælg et brugernavn (kun bogstaver, tal og understreg)" @@ -1055,7 +1055,7 @@ da: default: "Den hemmelige kode passede ikke med billedet" failed: "Den menneskelige kontrol mislykkedes" user: "Det hemmelige billede og koden var forskellige" - placeholder: "Indsæt billedværdi" + placeholder: "Skriv billedværdien" statistics: active_users_halfyear: "Aktive brugere halvårligt" active_users_monthly: "Aktive brugere månedligt" @@ -1129,7 +1129,7 @@ da: success: "Din konto er nu låst. Det kan tage 20 minutter for os at blive færdige med at lukke den helt. Tak fordi du prøvede Diaspora." wrong_password: "Det indtastede kodeord stemmer ikke overens med dit nuværende kodeord." edit: - also_commented: "nogen har kommenteret et indlæg du tidligere har kommenteret." + also_commented: "nogen kommenterer et indlæg du tidligere har kommenteret." auto_follow_aspect: "Aspekt for automatisk tilføjede kontakter:" auto_follow_back: "Følg automatisk brugere der følger dig" change: "Skift" @@ -1147,7 +1147,7 @@ da: no_turning_back: "Du kan ikke fortryde dette! Hvis du er helt sikker, indtast din adgangskode herunder." what_we_delete: "Vi sletter alle dine indlæg og profildata så hurtigt som overhovedet muligt. Dine kommentarer til andre folks indlæg vil forblive, men vil vise dit Diaspora-ID og ikke dit navn." close_account_text: "Luk konto" - comment_on_post: "... nogen har kommenteret dit indlæg." + comment_on_post: "nogen kommenterer dit indlæg." current_password: "Nuværende adgangskode" current_password_expl: "det du logger ind med ..." download_export: "Download min profil" diff --git a/config/locales/diaspora/de-CH.yml b/config/locales/diaspora/de-CH.yml new file mode 100644 index 000000000..4483360ad --- /dev/null +++ b/config/locales/diaspora/de-CH.yml @@ -0,0 +1,534 @@ +# Copyright (c) 2010-2013, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + + + +de-CH: + _applications: "Äpp's" + _contacts: "Kontäkt" + _services: "Dienscht" + account: "Konto" + activerecord: + errors: + models: + contact: + attributes: + person_id: + taken: "dörf nu eimol vorcho i dem Benutzer sine Kontäkt." + person: + attributes: + diaspora_handle: + taken: "isch scho vergeh." + request: + attributes: + from_id: + taken: "isch es duplikat vonere scho existierende aafrog." + reshare: + attributes: + root_guid: + taken: "Es isch guet, imfall - Du hesch de Post scho Wiitergseit!" + user: + attributes: + email: + taken: "isch scho vergeh." + person: + invalid: "isch ungültig" + username: + invalid: "isch ungültig, - Mer erlaubed nume Buechstabe, Nummere und Understrich." + taken: "isch scho vergeh." + admins: + admin_bar: + pages: "Siitä" + pod_stats: "Pod Statistik" + sidekiq_monitor: "Sidekiq-Monitor" + user_search: "Benutzer Suechi" + weekly_user_stats: "Wöchentlichi Benutzerstatistik" + stats: + 2weeks: "2 wuche" + 50_most: "50 meischt-benutzti täg's" + current_segment: "S'aktuelle segment het durchschnittlich %{post_yest} biiträg pto Benutzer, sit em %{post_day}" + daily: "Täglich" + display_results: "Zeig d'ergäbnis usem %{segment} segment" + go: "Los!" + month: "Monet" + tag_name: "Tag Name: %{name_tag} Azahl: %{count_tag}" + usage_statistic: "Nutzigs-statistik" + week: "Wuche" + user_search: + add_invites: "IIladige hinzuefüege" + email_to: "per email iilade" + weekly_user_stats: + current_server: "S'Momentane Serverdatum isch %{date}" + all_aspects: "Ali Aspekt" + are_you_sure: "Bisch sicher?" + are_you_sure_delete_account: "Bisch sicher, dass din Account lösche willsch ? - da chasch nüm rückgängig mache!" + aspect_memberships: + destroy: + failure: "Benutzer vom Aspekt entferne isch gschiiteret" + no_membership: "Han die uswählt Person i dem Aspekt nöd chönne finde." + success: "Benutzer erfolgriich vom Aspekt entfernt" + aspects: + add_to_aspect: + failure: "De Kontakt zum Aspekt hinzuefüege isch fehlgschlage." + success: "Erfolgrich de Kontakt zum Aspekt hinzuegfüegt." + aspect_listings: + add_an_aspect: "+ en Aspekt adde" + aspect_stream: + make_something: "Mach öppis" + stay_updated: "Bliib fresh" + stay_updated_explanation: "Din Haupt-Stream beinhaltet d'posts vo all dine Kontäkt, vo de täg's dene du folgsch und vo es paar kreative mit-diasporianerInne" + destroy: + failure: "%{name} het nöd chönne entfernt werde" + success: "%{name} isch erfolgriich entfernt worde." + edit: + aspect_list_is_not_visible: "Kontäkt i dem Aspekt chönd sich gegesiitig nöd gseh." + aspect_list_is_visible: "Kontäkt i dem Aspekt chönd sich gegesiitig gseh." + confirm_remove_aspect: "Bisch sicher, das de Aspekt lösche willsch?" + rename: "Umbenenne" + update: "Ändere" + updating: "am Ändere" + index: + donate: "Spende" + help: + any_problem: "Git's es Problem?" + contact_podmin: "Kontaktier de Admin vo dim Pod!" + do_you: "Hesch du:" + feature_suggestion: "en vorschlag für e %{link}?" + find_a_bug: "en %{link} gfunde?" + have_a_question: "en %{link}" + here_to_help: "d'Diaspora* Komunity isch do!" + mail_podmin: "Podmin email" + need_help: "Bruchsch Hilf?" + tag_bug: "en bug" + tag_feature: "e Verbesserig" + tag_question: "e Frog" + introduce_yourself: "Das isch din Stream. Gump ine und stell dich vor." + keep_pod_running: "Hilf %{pod} schnell d'sii und finanzier mitere monatliche chliine spend de schuss kafi wo's defür bruuched" + new_here: + follow: "Folg %{link} und heiss neui Benutzer bi Diaspora* willkomme !" + learn_more: "meh info's" + title: "Tuen neui Benutzer Wilkomme heisse" + services: + content: "Du chasch folgendi Dienst mit Diaspora* verbinde:" + heading: "Dienst verbinde" + welcome_to_diaspora: "Willkomme bi Diaspora*, %{name}!" + no_contacts_message: + community_spotlight: "Kommunity Schiiwörferliecht" + or_spotlight: "Oder du chasch mit %{link} Teile" + try_adding_some_more_contacts: "Du chasch sueche oder meh Kontäkt %{invite_link}" + you_should_add_some_more_contacts: "Du söttsch es paar meh Kontäkt adde" + seed: + acquaintances: "Bekannti" + family: "Familie" + friends: "Fründe" + work: "Arbet" + update: + failure: "Din Aspekt %{name} het en z'lange name zum chönne gspeicheret werde" + success: "Din Aspekt %{name} het erfolgriich chönne gänderet werde" + blocks: + create: + failure: "Ignoriere vo dem benutzer fehlgschlage" + success: "Ok, dä gsesch nüm !" + destroy: + failure: "Han nöd chönne ufhöhre dä benutzer z'ignoriere." + success: "Luegemer mol was die z'sägä hend! #sayhello" + bookmarklet: + explanation: "Poste en biitrag bi Diaspora vo egal wo i dem du dä Link => %{link} zu dine Läsezeiche hinzuefüegsch" + heading: "Läsezeichä" + post_something: "En biitrag in Diaspora* erstelle" + cancel: "Abbreche" + comments: + new_comment: + comment: "Kommentiere" + commenting: "Am Kommentiere..." + contacts: + index: + add_a_new_aspect: "en neue Aspekt mache" + all_contacts: "Ali Kontäkt" + community_spotlight: "Kommunity Schiiwörferliecht" + my_contacts: "Mini Kontäkt" + no_contacts: "Gseht uus als möstisch es paar Kontäkt adde" + no_contacts_message: "Lueg mal s'%{community_spotlight} aa" + only_sharing_with_me: "Eisiitig mit mir teilendi" + start_a_conversation: "Start es Gspröch" + title: "Kontäkt" + spotlight: + community_spotlight: "Kommunity Schiiwörferliecht" + suggest_member: "Mitglied vorschloh" + conversations: + create: + fail: "Ungültigi Nochricht" + no_contact: "Hoppla, du muesch zersch de Kontakt adde." + sent: "Nochricht gsendet" + index: + inbox: "Igang" + no_messages: "Kei Nochrichte" + new: + send: "Sende" + sending: "Sendend..." + subject: "worum gaht's ?" + to: "A" + show: + delete: "Gspröch lösche" + reply: "Antworte" + replying: "Am Antworte..." + date: + formats: + birthday: "%d. %B" + birthday_with_year: "%d. %B %Y" + fullmonth_day: "%d. %B" + delete: "Lösche" + email: "Email" + error_messages: + helper: + correct_the_following_errors_and_try_again: "Korrigier folgendi Errör's und versuechs nomel." + fill_me_out: "Füll mi uus" + find_people: "Find Mensche oder Tag's" + invitations: + a_facebook_user: "En Facebook benutzer" + check_token: + not_found: "Iilatigs-token isch nöd gfunde worde" + create: + empty: "Bitte mindestens ei email adresse iigä" + no_more: "Kei wiiteri Iilage." + note_already_sent: "A %{emails} sind scho e iiladig gschickt worde." + rejected: "Bi folgende adresse het's Problem geh: " + sent: "Illadige sind a folgendi adresse gsendet worde: %{emails}" + new: + comma_separated_plz: "Du chasch mehreri email adresse iigeh, wenns dur es komma trennsch." + invite_someone_to_join: "Tuen öpper zu Diaspora* Iilade!" + language: "Sproch" + paste_link: "Teil de link mit dine fründe zum sie zu Diaspora* iizlade, oder schrick ene de link direkt." + send_an_invitation: "En Iiladig sende" + layouts: + application: + back_to_top: "Zrugg zum Aafang" + powered_by: "Es lauft mir Diaspora*" + public_feed: "Öffentliche Diaspora* feed vo %{name}" + source_package: "Lad S'Quellcode-packet abe" + toggle: "Mobiili aasicht ii/us-schalte" + whats_new: "Was git's neus?" + header: + logout: "Abmelde" + profile: "Profil" + settings: "Iistelige" + limited: "Limitiert" + more: "Meh" + no_results: "Kei Resultat gfunde" + notifications: + comment_on_post: + one: "%{actors} het din biitrag %{post_link} kommentiert" + other: "%{actors} hend din biitrag %{post_link} kommentiert" + zero: "%{actors} het din biitrag %{post_link} kommentiert" + index: + and: "und" + mark_unread: "als gläsä markiere" + liked: + one: "%{actors} het din post %{post_link} gern" + other: "%{actors} hend din post %{post_link} gern" + zero: "%{actors} het din post %{post_link} gern" + mentioned: + one: "%{actors} het dich im post %{post_link} erwähnt" + other: "%{actors} hend dich im post %{post_link} erwähnt" + zero: "%{actors} het dich im post %{post_link} erwähnt" + private_message: + one: "%{actors} het dir e Nochricht gschickt." + other: "%{actors} hend dir e Nochricht gschickt." + zero: "%{actors} het dir e Nochricht gschickt." + reshared: + one: "%{actors} het din post %{post_link} wiitergseit." + other: "%{actors} hend din post %{post_link} wiitergseit." + started_sharing: + one: "de %{actors} het agfange mit dir Teile." + other: "%{actors} hend agfange mit dir Teile." + zero: "%{actors} het agfange mit dir Teile." + notifier: + a_post_you_shared: "en post" + click_here: "druck do" + comment_on_post: + reply: "Gib antwort oder lueg der de post vo %{name} aa." + confirm_email: + click_link: "Klick uf dä link zum dini neu email adresse %{unconfirmed_email} z'aktiviere" + subject: "Bitte aktivier dini neu email adresse %{unconfirmed_email}" + email_sent_by_diaspora: "Das e-mail isch vo %{pod_name} gsendet worde. Wenn du kei söttigi emails me willsch," + hello: "Hallo %{name}!" + invite: + message: |- + Hallo! + du bisch vo %{diaspora_id} iiglade worde Diaspora* biizträte. + Mit dem Link gohts los + [%{invite_url}][1] + + Oder füeg %{diaspora_id} zu dine Aspekt hinzue. wennd scho en Account hesch. + + Liebi grüess + De Diaspora* email roboter + + Ps. Für de fall das do gar nonig weisch was Diaspora* isch:[do][2] git's meh info's. + + [1]: %{invite_url} + [2]: %{diasporafoundation_url} + invited_you: "%{name} het dich zu Diaspora* iiglade" + liked: + liked: "%{name} het din post gern" + view_post: "Post aaluege >" + mentioned: + subject: "%{name} het dich uf Diaspora* erwähnt" + private_message: + reply_to_or_view: "Gib Antwort oder lueg die Underhaltig aa >" + reshared: + reshared: "%{name} het din post wiitergseit" + view_post: "Post aaluege >" + single_admin: + admin: "Din Diaspora* administrator" + subject: "E Nochricht über din Diaspora* Account:" + started_sharing: + sharing: "het aagfange mit dir teile!" + subject: "%{name} het uf Diaspora* aagfange mit dir teile" + view_profile: "lueg s'profil vo %{name} aa" + thanks: "Danke," + to_change_your_notification_settings: "zum dini benochrichtigungs-iistellige z'ändere" + nsfw: "NSFW" + ok: "OK" + people: + add_contact: + invited_by: "Du bisch iglade worde vo" + index: + looking_for: "Bisch uf de suech noch mit %{tag_link} täggde biiträg?" + no_one_found: "...und niemer isch gunde worde." + no_results: "Hey, du muesch noch öppisem sueche." + results_for: "Benutzer zum Thema %{search_term}" + searching: "Sueche, bitte bis geduldig..." + person: + thats_you: "Das bisch du!" + profile_sidebar: + bio: "Biografie" + born: "Geburi" + gender: "Gschlächt" + location: "Ort" + show: + closed_account: "Dä Account isch gschlosse worde." + does_not_exist: "Die Person existiert nöd!" + has_not_shared_with_you_yet: "%{name} het bis etz no kei posts mit dir teilt.." + photos: + create: + integrity_error: "Fotiupload isch fehlgschlage... Sicher das da e bild gsi isch?" + runtime_error: "Fotiupload isch fehlgschlage... Sicher das ali tasse im chuchichäschtli sind?" + type_error: "Fotiupload isch fehlgschlage... lueg mal ob's bild dezuegfüegt worde isch.." + destroy: + notice: "Foti glöscht." + new_photo: + empty: "{file} isch läär, bitte wähl d'dateie nomol ohni das us." + invalid_ext: "{file} het e ungültigi Dateiendig. Nur folgendi ändige sind erlaubt : {extensions}" + size_error: "{file} isch z'gross.. - Uploads dörfed Maximal {sizeLimit} sii." + new_profile_photo: + upload: "Lad es neus Profilfoti ue!" + show: + show_original_post: "Orginal-Post aazeige" + posts: + presenter: + title: "En post vo %{name}" + show: + reshare_by: "Wiitergseits vo %{author}" + privacy: "Privatsphäre" + profile: "Profil" + profiles: + edit: + allow_search: "Andere lüüt innerhalb vo Diaspora* erlaube noch dir z'sueche" + first_name: "Vorname" + last_name: "Nochname" + update_profile: "Profil Update" + your_bio: "Dini beschriibig" + your_birthday: "Din Geburtstag" + your_gender: "Dis Gschlächt" + your_location: "Wo du bisch" + your_name: "Din name" + your_photo: "dis Foti" + your_tags: "Beschriib dich in 5 wörter" + your_tags_placeholder: "Zum Biispiil: #diaspora #chuchichäschtli #kreativ #linux #musig" + update: + failed: "Aktualisierä vom Profil isch fehlschlage" + updated: "Pofil aktualisiert" + public: "Öffentlich" + reactions: + one: "%{count} Reaktion" + other: "%{count} Reaktione" + zero: "Kei Reaktion" + registrations: + closed: "Uf dem pod isch Account erröffnig deaktiviert" + create: + success: "Du bisch ez volle hahne bi Diaspora* debii !" + invalid_invite: "De iiladigslink wo'd erstellt hesch isch nüme gültig!" + new: + email: "Email" + enter_email: "Gib dini email adresse ii" + enter_password: "Passwort iigä (mindestens 6 zeiche)" + enter_password_again: "Nomol s'gliich passwort wie vorher iigä" + enter_username: "Wähl en benutzername uus (bestehend usschliesslich us: zahle, nummere, und understrich)" + password: "Passwort" + password_confirmation: "Passwort bestätigung" + sign_up: "Account mache" + username: "Benutzername" + reshares: + reshare: + deleted: "Originalpost isch vom Verfasser glöscht worde" + reshare_confirmation: "%{author}'s post wiitersägä ?" + reshared_via: "wiitergseit vo" + search: "Sueche" + services: + create: + already_authorized: "En benutzer mit de Diapsora id %{diaspora_id} het de Account mit name %{service_name} scho autorisiert." + failure: "Authentifizierig fehlgschlage." + success: "Authentifizierig erfolgriich." + destroy: + success: "Authentifikation erfolgriich glöscht." + failure: + error: "Bim verbinde zu dem Dienst het's en error geh.." + index: + disconnect: "Trenne" + edit_services: "Dienst bearbeite" + logged_in_as: "Iigloggt als %{nickname}" + really_disconnect: "%{service} trenne" + settings: "Iistelige" + shared: + invitations: + by_email: "via email" + invite_your_friends: "Lad dini Kollege ii" + invites: "Iiladige" + share_this: "Teil dä Link über email, blog oder soziali Netzwerk!" + public_explain: + atom_feed: "Atom feed" + control_your_audience: "Kontrolier dini Ziilgruppe" + logged_in: "Igglogt in %{service}" + manage: "Verbundeni dienst verwalte" + new_user_welcome_message: "Mach #hashtags zum dini post's z'tägge und lüüt mit ähnliche intresse finde. Erwähn anderi mensche mit @mentions" + outside: "Öfentlichi biiträg sind für's gsamte internet sichtbar." + share: "Teilä" + title: "Verbundeni Dienst ufsetzä" + visibility_dropdown: "Zum iistelle wär din post gseh dörf benutz s'dropdown menü. (Mer empfehled din erschtä post öffentlich z'mache (wellen susch nemert gseht ^^ ))" + publisher: + discard_post: "Posting abbreche" + new_user_prefill: + hello: "Hallo zämä Ich bin %{new_user_tag}. " + i_like: "Ich interessier mich für %{tags} " + invited_by: "Danke für d'iilagig, " + newhere: "neudo" + posting: "Am poste..." + share: "Teilä" + upload_photos: "Fotis Uelade" + whats_on_your_mind: "Was lauft?" + stream_element: + via: "via %{link}" + via_mobile: "über's händy" + status_messages: + create: + success: "Erfolgriich erwähnt: %{names}" + new: + mentioning: "Erwähnt: %{person}" + too_long: "Bitte chürz din post uf weniger als %{count} zeichä. Im moment heter %{current_length}." + streams: + activity: + title: "Mini Aktivitätä" + aspects: + title: "mini Aspekt" + aspects_stream: "Aspekt" + comment_stream: + title: "Kommentierti Post's" + community_spotlight_stream: "Kommunity Schiiwörferliecht" + followed_tag: + add_a_tag: "En tag adde" + follow: "Folge" + title: "#tags dene du folgsch" + followed_tags_stream: "#tags dene du folgsch" + like_stream: + title: "\"gfallt mer\"-stream" + mentioned_stream: "@erwähnige" + mentions: + title: "@erwähnige" + multi: + title: "stream" + public: + title: "Öffentlichi aktivität" + tags: + title: "Täggti Biiträg: %{tags}" + tags: + show: + follow: "#%{tag} folge" + none: "De läär tag existiert nöd!" + stop_following: "Ufhöre #%{tag} z'folge" + username: "Benutzername" + users: + confirm_email: + email_confirmed: "Email %{email} isch aktiviert" + email_not_confirmed: "Email het nöd chönne aktiviert werde. Falsche Link?" + destroy: + no_password: "Bitte dis aktuelle passwort iigäh zum de account schlüsse." + success: "Dis Konto isch gschperrt. Es chan ~ 20 Minute goh bevor din Account endgültig gschlosse isch. Danke das du dich uf Diaspora* versuecht hesch." + wrong_password: "Das Passwort stimmt nöd mit dim aktuelle passwort überein." + edit: + also_commented: "öpper en post kommentiert wo du scho kommentiert häsch" + auto_follow_aspect: "Apekt für benutzer mit dene du automatisch hesch agfange teile" + auto_follow_back: "Automatisch mit benutzer \"zruggteile\" wo agfange hend mit dir z'teile" + change: "ändere" + change_email: "Email ändere" + change_language: "Sproch ändere" + change_password: "Passwort ändere" + character_minimum_expl: "mues mindestens sächs zeiche ha" + close_account: + dont_go: "Hey, bitte hau nöd ab!" + lock_username: "Din benutzername wird gsperrt. Niemer wird d'möglichkeit ha zum die gliich ID uf dem pod nomol z'registriere." + locked_out: "Du wirsch us dim account abgmeldet und usgsperrt bis er glöscht worde isch." + make_diaspora_better: "Üs wär's sehr vill lieber du würsch bliibe und mithelfe Diaspora* besser z'mache anstatt z'goh. - Wenn aber würkli wotsch, das passiert als nöchschts:" + mr_wiggles: "Wenn du gosch wird de Mr.Wiggles ganz truurig" + no_turning_back: "Es git keis zrugg! Wenn du dir würkli sicher bisch, gib dis passwort une ii." + what_we_delete: "Mer lösched all dini biiträg und profildate so bald als möglich. Dini kommentär wo du under de posts vo andere lüüt gmacht hesch werded immerno aazeigt aber mit dinere Diaspora* ID assoziiert anstatt dim name." + close_account_text: "Account schlüsse" + comment_on_post: "öpper en post vo dir kommentiert" + current_password: "aktuell's passwort" + current_password_expl: "Das mit demm du di aameldisch..." + edit_account: "Account bearbeite" + email_awaiting_confirmation: "Mer dir en link noch %{unconfirmed_email} gschikt. Bis du de enthalteni link göffnet hesch und so die neu adresse aktiviersch, sendemer wiiterhin a dini alt adresse %{email}" + export_data: "Date exportiere" + following: "\"Teile\"-Iistelige" + liked: "öpper en post vo dir \"mag\"" + mentioned: "du @erwähnt wirsch" + new_password: "Neus Passwort" + private_message: "du e privati nochricht überchunsch" + receive_email_notifications: "Email-benochritigunge empfange wänn:" + reshared: "öpper din post wiiterseit" + show_community_spotlight: "Kommunity Schiiwörferliecht im stream aazeige" + show_getting_started: "Iistiiger hiiwiis wieder aktivierä" + started_sharing: "öpper mit dir aafangt teile" + stream_preferences: "Stream Iistellige" + your_email: "Dini Email Adresse" + your_handle: "Dini Diaspora* ID" + getting_started: + awesome_take_me_to_diaspora: "Grossartig! bring mich zu Diaspora*" + community_welcome: "d'Diapora\"-Mensche sind froh dich an board zha!" + hashtag_explanation: "Mit hashtags chasch du über Sache rede und dini Intresse folge. Usserdem eignet si sich hervorragend zum neui lüüt uf Diaspora* z'finde." + hashtag_suggestions: "Versuech's mal tags wie zb. #kunscht, #film, #chääs etc.." + well_hello_there: "Halli Hallo !" + what_are_you_in_to: "Was machsch so?" + who_are_you: "Wär bisch du?" + privacy_settings: + ignored_users: "Ignorierti benutzer" + stop_ignoring: "Ignoriere ufhebe" + title: "Privatsphäre iistelige" + public: + does_not_exist: "%{username} existiert nöd." + update: + email_notifications_changed: "Email benochrichtigunge gänderet" + follow_settings_changed: "Folge-Iistelige gänderet" + follow_settings_not_changed: "Folge-Iistelige änderig fehlgschlage" + language_changed: "Sproch gänderet" + language_not_changed: "Änderig vo de Sproch isch fehlgschlage" + password_changed: "Passwort g'änderet. Du chasch di jetzt mit dim neue Passwort iilogge." + password_not_changed: "Passwort änderig fehlgschlage" + settings_not_updated: "Änderig vo de Iistellige isch fehlgschlage" + settings_updated: "Iistellige aktualisiert" + unconfirmed_email_changed: "Email g'änderet. Mue no aktiviert werde" + unconfirmed_email_not_changed: "Email änderig fehlgschlage" + will_paginate: + next_label: "nögscht »" + previous_label: "« zrugg" \ No newline at end of file diff --git a/config/locales/diaspora/de.yml b/config/locales/diaspora/de.yml index 41ec1a54a..6e68d73b3 100644 --- a/config/locales/diaspora/de.yml +++ b/config/locales/diaspora/de.yml @@ -418,7 +418,7 @@ de: what_is_a_mention_a: "Eine Erwähnung ist ein Link zu der Profilseite einer Person, der in einem Beitrag erscheint. Wenn jemand erwähnt wird, erhält er eine Benachrichtigung, die seine Aufmerksamkeit auf den Beitrag lenkt." what_is_a_mention_q: "Was ist eine „Erwähnung“?" miscellaneous: - back_to_top_a: "Ja. Nachdem du auf einer Seite nach unten gescrollt hast, klicke auf den grauen Pfeil, der in der unteren rechten deines Browserfensters erscheint." + back_to_top_a: "Ja. Nachdem du auf einer Seite nach unten gescrollt hast, klicke auf den grauen Pfeil, der unteren rechts im Browserfenster erscheint." back_to_top_q: "Gibt es eine Möglichkeit, schnell wieder an den Seitenanfang zu kommen?" diaspora_app_a: "Es gibt einige Android-Apps in einem sehr frühen Entwicklungsstadium. Einige Projekte sind bereits seit langem abgebrochen und funktionieren deshalb nicht richtig mit der aktuellen diaspora*-Version. Erwarte von diesen Apps im Moment nicht allzu viel. Zur Zeit ist die beste Möglichkeit, um auf diaspora* von deinem Mobilgerät zuzugreifen, die Seite in einem Browser aufzurufen, da wir eine mobile Version gestaltet haben, die auf allen Geräten gut funktionieren sollte. Für iOS gibt es im Moment keine Apps. Noch einmal, diaspora* sollte sich gut von deinem Browser aus bedienen lassen." diaspora_app_q: "Gibt es eine diaspora* App für Android oder iOS?" @@ -431,7 +431,7 @@ de: find_people_a: "Lade deine Freunde mit dem E-Mail-Link in der Seitenleiste ein. Folge #Tags, um Andere zu entdecken, die deine Interessen teilen und füge Leute, die Sachen posten, die dich interessieren, zu deinen Aspekten hinzu. Schreibe in einem öffentlichen Post, dass du #NeuHier bist." find_people_q: "Ich bin gerade erst einem Pod beigetreten, wie finde ich nun Leute zum Teilen?" title: "Pods" - use_search_box_a: "Wenn du deren vollständige diaspora* ID (z.B. benutzername@podname.org) kennst, kannst du sie finden, indem du danach suchst. Wenn du dich auf dem selben Pod befindest, reicht es, wenn du nur nach dem Benutzernamen suchst. Als Alternative kannst du auch nach ihrem Profilnamen (dem angezeigten Namen) suchen. Wenn eine Suche beim ersten Mal keine Ergebnisse liefert, dann versuch es nochmal." + use_search_box_a: "Kennst du deren vollständige diaspora* ID (z.B. benutzername@podname.org), kannst du sie durch suchen finden. Bist du auf dem selben Pod, kannst du direkt nach dem Benutzernamen suchen. Alternativ kannst du auch nach ihrem Profilnamen (dem angezeigten Namen) suchen. Wenn eine Suche beim ersten Mal keine Ergebnisse liefert, dann versuch es nochmal." use_search_box_q: "Wie benutze ich das Suchfeld, um bestimmte Personen zu finden?" what_is_a_pod_a: "Ein Pod ist ein Server, auf dem die diaspora*-Software läuft und der mit dem diaspora*-Netzwerk verbunden ist. „Pod“ ist eine Metapher für Hülsen von Pflanzen, die mehrere Samen enthalten, so wie der Server mehrere Benutzerkonten enthält. Es gibt viele verschiedene Pods. Du kannst Kontakte von anderen Pods hinzufügen und mit ihnen kommunizieren. Du musst nicht Benutzerkonten auf verschiedenen Pods erstellen! (Du kannst dir einen diaspora*-Pod wie einen Mail-Anbieter vorstellen: Es gibt öffentliche Pods, private Pods, und mit ein wenig Aufwand kannst du deinen eigenen betreiben.)" what_is_a_pod_q: "Was ist ein Pod?" @@ -481,7 +481,7 @@ de: who_sees_post_q: "Wenn ich einen Beitrag in einen Aspekt schreibe (privat), wer kann ihn sehen?" private_profiles: title: "Private Profile" - whats_in_profile_a: "Deine Beschreibung, dein Ort, dein Geschlecht und dein Geburtstag. Das sind die Sachen im unteren Bereich der Profileinstellungen. Alle diese Informationen sind freiwillig – Du entscheidest, ob du sie preisgibst. Angemeldete Benutzer, die sich in einem deiner Aspekte befinden, sind die Einzigen, die dein privates Profil sehen können. Sie werden auf deiner Profilseite außerdem neben deinen öffentlichen Beiträgen auch private Beiträge von dir sehen können, die an Aspekte gerichtet sind, in denen sie sich befinden." + whats_in_profile_a: "Deine Beschreibung, dein Ort, Geschlecht und Geburtstag. Das sind die Dinge aus dem unteren Bereich der Profileinstellungen. Alle diese Informationen sind freiwillig – Du entscheidest, ob du sie preisgibst. Angemeldete Benutzer, aus deinen Aspekten, sind die Einzigen, die dein privates Profil sehen können. Sie werden auf deiner Profilseite außerdem neben deinen öffentlichen Beiträgen auch private Beiträge von dir sehen können, die an Aspekte gerichtet sind, in denen sie sich befinden." whats_in_profile_q: "Was ist in meinem privaten Profil zu sehen?" who_sees_profile_a: "Jeder angemeldete Benutzer, mit dem du teilst (den du zu einem deiner Aspekte hinzugefügt hast). Leute, die dir folgen, denen du aber nicht folgst, werden nur deine öffentlichen Informationen sehen." who_sees_profile_q: "Wer kann mein privates Profil sehen?" diff --git a/config/locales/diaspora/eo.yml b/config/locales/diaspora/eo.yml index d2ae114af..d33a0419b 100644 --- a/config/locales/diaspora/eo.yml +++ b/config/locales/diaspora/eo.yml @@ -42,6 +42,7 @@ eo: admin_bar: pages: "Paĝoj" pod_stats: "Statistiko de 'pod' (Diaspora-servilo)" + sidekiq_monitor: "Sidekiq monitoro" user_search: "Serĉo de uzanto" weekly_user_stats: "Semajna statistiko de uzanto" stats: @@ -88,7 +89,7 @@ eo: current_server: "Aktuala dato de servilo estas %{date}" all_aspects: "Ĉiuj aspektoj" are_you_sure: "Ĉu vi certas?" - are_you_sure_delete_account: "Ĉu vi certe volas forigi vian konton? Vi ne povas malfari tion!" + are_you_sure_delete_account: "Ĉu vi certe volas forigi vian konton? Vi ne povos malfari tion!" aspect_memberships: destroy: failure: "Forigi personon el aspekto malsukcesis" @@ -111,9 +112,9 @@ eo: aspect_list_is_not_visible: "la aspekto-listo estas kaŝita al la aliaj en ĉi tiu aspekto" aspect_list_is_visible: "la aspekto-listo estas montrata al la aliaj en ĉi tiu aspekto" confirm_remove_aspect: "Ĉu vi certe volas forigi ĉi tiun aspekton?" - rename: "alinomigi" + rename: "Renomigi" update: "ĝisdatigi" - updating: "ĝisdatigante" + updating: "ĝisdatigado" index: donate: "Donaci" help: @@ -136,7 +137,7 @@ eo: new_here: follow: "Sekvu %{link} kaj bonvenigu novajn uzantojn al Diaspora*!" learn_more: "Lerni pli" - title: "Bonvenigu novajn uzantojn" + title: "Bonvenu novajn uzantojn" services: content: "Vi povas konekti la postajn servojn kun DIASPORA*:" heading: "Konekti Servojn" @@ -217,9 +218,13 @@ eo: help: foundation_website: "Retejo de Fondaĵo Diaspora" getting_help: + get_support_a_irc: "aliĝu al ni en %{irc} (rektbabilejo)" get_support_a_website: "vizitu nian %{link}" + get_support_a_wiki: "serĉi en %{link}" getting_started_a: "Vi bonŝancas. Provu la %{tutorial_series} en nia projektrejejo. Ĝi enkondukos vin paŝo post paŝo tra la aliĝilon kaj instruos vin ĉiujn bazajn bezonatajn informojn pri la uzado de diaspora*." getting_started_q: "Help! Mi bezonas bazan helpon por komenci!" + title: "Ricevo de helpo" + getting_started_tutorial: "\"Instrukcio por komencantoj\"" here: "ĉi tie" irc: "IRC" markdown: "Markdown" diff --git a/config/locales/diaspora/fr.yml b/config/locales/diaspora/fr.yml index 137627b4f..2d4634520 100644 --- a/config/locales/diaspora/fr.yml +++ b/config/locales/diaspora/fr.yml @@ -10,7 +10,7 @@ fr: _help: "Aide" _services: "Services" _statistics: "Statistiques" - _terms: "conditions d'utilisation" + _terms: "Conditions d'utilisation" account: "Compte" activerecord: errors: @@ -238,7 +238,7 @@ fr: new_here: follow: "Suivez %{link} et souhaitez la bienvenue aux nouveaux utilisateurs de diaspora* !" learn_more: "En savoir plus" - title: "Accueillir les nouveaux venus" + title: "Accueillir les nouveaux" services: content: "Vous pouvez connecter les services suivants à diaspora* :" heading: "Connecter des services" @@ -418,7 +418,7 @@ fr: miscellaneous: back_to_top_a: "Oui. Après avoir atteint le bas de page, cliquez sur la flèche grise qui apparaît dans le bouton dans le coin à droite de votre fenêtre de navigation." back_to_top_q: "Il y a t-il un moyen de rapidement revenir en haut d'une page après avoir atteint le bas de celle-ci ?" - diaspora_app_a: "Il y a quelques applications Android encore dans les toutes premières phases de développement. Plusieurs sont des projets abandonnés depuis un peu de temps et ne marchent donc pas bien avec la version actuelle de diaspora*. N'en attendez pas trop pour le moment. Actuellement, la meilleure façon accéder à diaspora* à partir d'un téléphone portable est à travers un navigateur web car nous avons créé une version mobile du site qui devrait fonctionner correctement sur tous les appareils. Il n'y a pour l'instant aucune application pour iOS. Encore une fois, diaspora* devrait marcher correctement via votre navigateur web." + diaspora_app_a: "Il y a quelques applications Android encore dans les toutes premières phases de développement. Plusieurs sont des projets abandonnés depuis un peu de temps et ne marchent donc pas bien avec la version actuelle de diaspora*. N'en attendez pas trop pour le moment. Actuellement, la meilleure façon pour accéder à diaspora* à partir d'un téléphone portable est à travers un navigateur web car nous avons créé une version mobile du site qui devrait fonctionner correctement sur tous les appareils. Il n'y a pour l'instant aucune application pour iOS. Encore une fois, diaspora* devrait marcher correctement via votre navigateur web." diaspora_app_q: "Existe t-il une application diaspora* pour Android ou iOS ?" photo_albums_a: "Non, pas actuellement. Cependant vous pouvez voir l'ensemble des images d'un utilisateur à partir de la section Photos de la barre latérale de son profil." photo_albums_q: "Il y a t-il des albums photos ou vidéos ?" @@ -438,7 +438,7 @@ fr: char_limit_services_q: "Quelle est la limite de caractères pour les messages partagés avec un service connecté qui a une limite de caractères plus petite ?" character_limit_a: "65,535 caractères. C'est 65,395 caractères de plus que Twitter ! ;)" character_limit_q: "Quelle est la limite de caractères pour un message ?" - embed_multimedia_a: "Vous pouvez généralement juste coller l'URL (p. ex. : http://www.youtube.com/watch?v=nnnnnnnnnnn) au sein de votre message et la vidéo ou le contenu audio sera intégré automatiquement. Parmi les supportés, on trouve : Youtube, Vimeo, SoundCloud, Flickr et quelques autres. Diaspora* utilise oEmbed pour cette fonctionnalité. Nous ajoutons constamment le support de nouveaux sites. Souvenez-vous de toujours publier des liens complets et simples : pas de lien raccourci, pas d'opérateur après l'URL de base, et laissez passer un peu de temps avant de rafraîchir la page après avoir publié un message pour voir l'aperçu." + embed_multimedia_a: "Vous pouvez généralement juste coller l'URL (p. ex. : http://www.youtube.com/watch?v=nnnnnnnnnnn) au sein de votre message et la vidéo ou le contenu audio sera intégré automatiquement. Les sites supportés sont, entre autres : Youtube, Vimeo, SoundCloud et Flickr. Diaspora* utilise oEmbed pour cette fonctionnalité. De nouveaux sites sont régulièrements ajoutés. Publiez toujours des liens complets et simples : pas de lien raccourci, pas d'opérateur après l'URL de base, et attendez un peu avant de rafraîchir la page pour voir l'aperçu." embed_multimedia_q: "Comment puis-je intégrer une vidéo, de l'audio, ou tout autre contenu multimédia dans un message ?" format_text_a: "En utilisant un système simplifié appelé %{markdown}. Vous pouvez trouver la syntaxe complète de Markdown %{here}. Le bouton de prévisualisation est vraiment pratique car vous pourrez voir à quoi votre message va ressembler avant de le publier." format_text_q: "Comment puis-je formater le texte de mes messages (gras, italique, etc.) ?" @@ -629,9 +629,9 @@ fr: other: "%{actors} ont également commenté sur le message %{post_link} de %{post_author}." zero: "%{actors} a également commenté sur le message %{post_link} de %{post_author}." also_commented_deleted: - one: "%{actors} a commenté votre message supprimé." - other: "%{actors} ont commenté votre message supprimé." - zero: "%{actors} a commenté votre message supprimé." + one: "%{actors} a commenté un message supprimé." + other: "%{actors} ont commenté un message supprimé." + zero: "%{actors} a commenté un message supprimé." comment_on_post: one: "%{actors} a commenté votre message %{post_link}." other: "%{actors} ont commenté votre message %{post_link}." @@ -978,7 +978,7 @@ fr: no_services_available: "Il n'y a aucun service disponible sur ce pod." not_logged_in: "Vous n'êtes pas connecté actuellement." really_disconnect: "Déconnecter %{service} ?" - services_explanation: "Se connecter à des services vous donne la possibilité d'y publier vos messages depuis diaspora*." + services_explanation: "Se connecter à des services tiers vous donne la possibilité d'y publier vos messages depuis diaspora*." share_to: "Partager avec %{provider}" title: "Gérer les services connectés" provider: @@ -1107,7 +1107,7 @@ fr: destroy: no_password: "Veuillez introduire votre mot de passe actuel pour fermer votre compte." success: "Votre compte est verrouillé. Cela peut nous prendre jusqu'à vingt minutes pour finaliser sa fermeture. Merci d'avoir essayé diaspora*." - wrong_password: "Le mot de passe saisi ne correspond pas à votre mot de passe actuel." + wrong_password: "Le mot de passe saisi ne correspond pas à votre mot de passe actuel." edit: also_commented: "quelqu'un commente un message que vous avez déjà commenté." auto_follow_aspect: "Aspect pour les utilisateurs que vous suivez automatiquement :" @@ -1120,7 +1120,7 @@ fr: character_minimum_expl: "doit comporter au moins six caractères" close_account: dont_go: "Hé, s'il vous plaît, ne partez pas !" - lock_username: "Votre nom d'utilisateur sera bloqué. Vous ne pourrez pas créer un nouveau compte sur ce pod avec le même identifiant." + lock_username: "Votre nom d'utilisateur sera bloqué. Vous ne pourrez pas créer un nouveau compte sur ce pod avec le même nom." locked_out: "Vous serez déconnecté et votre compte sera inaccessible jusqu'à sa suppression." make_diaspora_better: "Nous aimerions beaucoup que vous restiez pour nous aider à améliorer diaspora* plutôt que de nous quitter. Toutefois, si c'est vraiment ce que vous souhaitez, voilà ce qui va se passer :" mr_wiggles: "Mr Wiggles sera triste de vous voir partir." diff --git a/config/locales/diaspora/hy.yml b/config/locales/diaspora/hy.yml index c307fa3f6..81af4011d 100644 --- a/config/locales/diaspora/hy.yml +++ b/config/locales/diaspora/hy.yml @@ -148,12 +148,30 @@ hy: contact_developer: "Պետք է հավելվածը մշակողին ուղարկես հետեւյալ սխալի մանրամասն հաղորդագրությունը`" title: "Օհ, ինչ-որ բան սխալ գնաց ։Չ" scopes: + aud: + description: "Սա aud թույլատվություն է տալիս ծրագրին" + name: "aud" + name: + description: "Սա անվան թույլտվություն է տալիս ծրագրին" + name: "անուն" + nickname: + description: "Սա մականվան թույլտվություն է տալիս ծրագրին" + name: "մականուն" openid: description: "Սա կթողնի հավելվածին կարդալ քո հիմնական էջը" name: "հիմնական էջը" + picture: + description: "Սա նկարի թույլտվություն է տալիս ծրագրին" + name: "նկար" + profile: + description: "Սա կթողնի հավելվածին կարդալ քո ընդլայնված էջը" + name: "ընդլայնված էջ" read: description: "Սա կթողնի հավելվածին կարդալ քո լրահոսը, քո զրույցները ու քո ամբողջական էջը" name: "կարդալ անձնական էջը, լրահոսն ու զրույցները" + sub: + description: "Սա sub թույլատվություն է տալիս ծրագրին" + name: "sub" write: description: "Սա կթողնի հավելվածին ուղարկել նոր գրառումները, ստեղծել նոր զրույցներ ու ուղարկել արձագանքները" name: "ուղարկել գրառումները, զրույցներն ու արձագանքները" @@ -222,7 +240,7 @@ hy: title: "Ողջունի՛ր նորեկներին" services: content: "Կարող ես միացնել հետևյալ ծառայությունները դիասպորա*յին՝" - heading: "Միացնել ծառայությունները" + heading: "Ծառայություններ միացնել" welcome_to_diaspora: "Բարի գալուստ դիասպորա*, %{name} ջան։" no_contacts_message: community_spotlight: "համայնքի ակնառու օգտատերերի" @@ -591,7 +609,7 @@ hy: layouts: application: back_to_top: "Թռնել վերև" - be_excellent: "Հարգե՜նք զմիմյանս ♥" + be_excellent: "Հարգալից եղեք միմյանց նկատմամբ ♥" powered_by: "Գործում է դիասպորա*յի օգնությամբ" public_feed: "%{name}-ի` դիասպորա*յի հրապարակային հոսքը" source_package: "Ներբեռնել սկզբնական կոդի փաթեթը" @@ -756,6 +774,7 @@ hy: subject: "%{name} նշել է քեզ գրառման մեջ դիասպորա*յում" private_message: reply_to_or_view: "Պատասխանիր կամ տես այս խոսակցությունը >" + subject: "Նոր անձնական նամակ ունես" remove_old_user: body: |- Ողջույն։ @@ -819,7 +838,7 @@ hy: searching: "Փնտրվում է, խնդրում ենք լինել համբերատար..." send_invite: "Ոչ մի արդյո՞ւնք։ Հրավե՛ր ուղարկիր։" person: - thats_you: "Դա դու՛ ես։" + thats_you: "սա դու ես" profile_sidebar: bio: "Կենսագրություն" born: "Ծննդյան ամսաթիվ" diff --git a/config/locales/diaspora/ia.yml b/config/locales/diaspora/ia.yml index 998d20ba0..fcc30506a 100644 --- a/config/locales/diaspora/ia.yml +++ b/config/locales/diaspora/ia.yml @@ -177,9 +177,9 @@ ia: aspect_list_is_not_visible: "Le contactos in iste aspecto non pote vider le un le altere." aspect_list_is_visible: "Le contactos in iste aspecto pote vider le un le altere." confirm_remove_aspect: "Es tu secur de voler deler iste aspecto?" - rename: "renominar" - update: "actualisar" - updating: "actualisation in curso" + rename: "Renominar" + update: "Actualisar" + updating: "Actualisation in curso" index: donate: "Donar" help: @@ -208,10 +208,10 @@ ia: heading: "Connecter servicios" welcome_to_diaspora: "Benvenite a diaspora*, %{name}!" no_contacts_message: - community_spotlight: "usatores in evidentia" + community_spotlight: "Usatores in evidentia" invite_link_text: "invitar" or_spotlight: "O tu pote divider con %{link}" - try_adding_some_more_contacts: "Tu pote cercar o invitar plus contactos." + try_adding_some_more_contacts: "Tu pote cercar o %{invite_link} plus contactos." you_should_add_some_more_contacts: "Tu deberea adder plus contactos!" seed: acquaintances: "Cognoscitos" @@ -453,7 +453,7 @@ ia: deselect_aspect_posting_q: "Que eveni quando io dismarca un o plus aspectos quando io face un entrata public?" find_public_post_a: "Tu entratas public apparera in le fluxos de omnes qui te seque. Si tu include #etiquettas in tu message public, omnes qui seque iste etiquettas trovara tu entrata in lor fluxos. Cata entrata public anque ha un adresse URL specific al qual omnes pote acceder, mesmo sin aperir session; assi, es possibile mitter ligamines a entratas public directemente in Twitter, blogs, etc. Le entratas public pote anque esser includite in le indices del motores de recerca del web." find_public_post_q: "Como pote altere personas trovar mi entrata public?" - see_comment_reshare_like_a: "Omne usator de diaspora* e omne alteres connectite a internet. Le commentos, appreciationes e repetitiones de entratas public es etiam public." + see_comment_reshare_like_a: "Le commentos, appreciationes e repetitiones de entratas public es etiam public. Omne usator de diaspora* e omne alteres sur internet pote vider tu interactiones con un entrata public." see_comment_reshare_like_q: "Quando io commenta, repete o apprecia un entrata public, qui pote vider lo?" title: "Entratas public" who_sees_post_a: "Omne usator de internet pote vider un entrata que tu marca como public, dunque, sia secur que tu vole vermente que tu message es public. Isto es un optime modo de attinger tote le mundo." @@ -1017,7 +1017,7 @@ ia: make_diaspora_better: "Nos ha besonio de adjuta pro meliorar diaspora*, dunque, per favor contribue in loco de quitar. Si tu vermente vole quitar, nos vole informar te de lo que evenira." mr_wiggles: "Sr. Wiggles essera triste de vider te partir" no_turning_back: "Tu ha arrivate al puncto de non retorno." - what_we_delete: "Nos va deler, le plus tosto possibile, tote le entratas e datos de profilo pertinente a te. Tu commentos remanera, ma essera associate con tu ID de diaspora* in loco de tu nomine." + what_we_delete: "Nos va deler, le plus tosto possibile, tote le entratas e datos de profilo pertinente a te. Tu commentos sur le entratas de altere personas remanera, ma essera associate con tu ID de diaspora* in loco de tu nomine." close_account_text: "Clauder conto" comment_on_post: "un persona commenta un entrata tue" current_password: "Contrasigno actual" @@ -1043,7 +1043,7 @@ ia: reshared: "un persona repete un entrata tue" show_community_spotlight: "Monstrar \"Usatores in evidentia\" in fluxo" show_getting_started: "Monstrar le avisos \"Como initiar\"" - someone_reported: "alcuno ha inviate un reporto" + someone_reported: "alcuno invia un reporto" started_sharing: "un persona comencia a divider con te" stream_preferences: "Preferentias de fluxo" your_email: "Tu adresse de e-mail" diff --git a/config/locales/diaspora/ja.yml b/config/locales/diaspora/ja.yml index 69442e62a..c5d689077 100644 --- a/config/locales/diaspora/ja.yml +++ b/config/locales/diaspora/ja.yml @@ -10,7 +10,7 @@ ja: _help: "ヘルプ" _services: "サービス" _statistics: "統計" - _terms: "条件" + _terms: "規約" account: "アカウント" activerecord: errors: @@ -18,7 +18,7 @@ ja: contact: attributes: person_id: - taken: "このユーザの他の連絡先と重複してはいけません。" + taken: "このユーザーの他の連絡先と重複してはいけません。" person: attributes: diaspora_handle: @@ -38,7 +38,7 @@ ja: reshare: attributes: root_guid: - taken: "えっ? すでにこの投稿を共有しています!" + taken: "えっ? すでにこの投稿をシェアしています!" user: attributes: email: @@ -78,8 +78,8 @@ ja: other: "%{count} 投稿" zero: "投稿なし" shares: - other: "%{count} 共有" - zero: "共有なし" + other: "%{count} シェア" + zero: "%{count} シェア" tag_name: "タグ名: %{name_tag} 件数: %{count_tag}" usage_statistic: "利用統計" users: @@ -243,7 +243,7 @@ ja: no_contacts_message: community_spotlight: "コミュニティスポットライト" invite_link_text: "招待" - or_spotlight: "または%{link}に共有することができます" + or_spotlight: "または%{link}にシェアすることができます" try_adding_some_more_contacts: "もっと連絡先を検索または%{invite_link}できます。" you_should_add_some_more_contacts: "もっと連絡先を追加しましょう!" seed: @@ -280,7 +280,7 @@ ja: no_contacts: "連絡先を追加する必要があるようです!" no_contacts_in_aspect: "まだこのアスペクトに連絡先はありません。以下は、このアスペクトに追加することができる、既存の連絡先のリストです。" no_contacts_message: "%{community_spotlight}をチェックアウトする" - only_sharing_with_me: "自分だけに共有" + only_sharing_with_me: "自分だけにシェア" start_a_conversation: "会話を開始する" title: "連絡先" user_search: "連絡先検索" @@ -327,13 +327,13 @@ ja: helper: correct_the_following_errors_and_try_again: "次の問題を解決してからやり直してください。" need_javascript: "このウェブサイトは正常に機能するためにJavaScriptが必要です。 JavaScriptを無効にした場合は、有効にしてこのページを更新してください。" - fill_me_out: "記入して" + fill_me_out: "記入してください" find_people: "人や #タグ を探す" help: account_and_data_management: close_account_a: "設定ページの一番下に移動し、「アカウントを閉じる」ボタンをクリックします。手続きを完了するために、パスワードを入力するように求められます。覚えておいてください、アカウントを閉じた場合、そのポッドにあなたのユーザー名で再登録することはできません。" close_account_q: "私の種子 (アカウント) を削除する方法は?" - data_other_podmins_a: "あなたが別のポッドの誰かと共有されると、あなたが彼らと共有した投稿や、あなたのプロフィールデータのコピーがそのポッドにに保存 (キャッシュ) され、そのポッドのデータベース管理者にアクセス可能になります。あなたが投稿やプロフィールデータを削除すると、それはあなたのポッドから削除され、それが以前に保存されていた他のポッドに削除要求が送信されます。あなたの画像は、あなた自身のポッドを除き、保存されることはありません。そのリンクのみが、他のポッドに送信されます。" + data_other_podmins_a: "あなたが別のポッドの誰かとシェアされると、あなたが彼らとシェアした投稿や、あなたのプロフィールデータのコピーがそのポッドにに保存 (キャッシュ) され、そのポッドのデータベース管理者にアクセス可能になります。あなたが投稿やプロフィールデータを削除すると、それはあなたのポッドから削除され、それが以前に保存されていた他のポッドに削除要求が送信されます。あなたの画像は、あなた自身のポッドを除き、保存されることはありません。そのリンクのみが、他のポッドに送信されます。" data_other_podmins_q: "他のポッドの管理者は、私の情報を見ることができますか?" data_visible_to_podmin_a: "一言でいえば: すべて。ポッド間の通信は、常に (SSLとダイアスポラ*独自の転送の暗号化を使用して) 暗号化されていますが、ポッド上のストレージのデータは暗号化されていません。彼らが望めば、あなたのポッドのデータベース管理者 (通常、ポッドを実行している人) は、(ユーザーデータを格納するほとんどのウェブサイトのように) すべてのプロフィールデータとあなたが投稿したすべてにアクセスすることができます。あなたのデータを預けても十分に信頼できる管理者のポッドを選ぶことができるように、あなたがサインアップするポッドに選択肢を与えているのはこのためです。あなた独自のポッドを実行すると、データベースへのアクセスを制御することができるため、より高いプライバシーを提供します。" data_visible_to_podmin_q: "私のポッド管理者はどのくらい私の情報を見ることができますか?" @@ -355,7 +355,7 @@ ja: person_multiple_aspects_q: "複数のアスペクトに人を追加することはできますか?" post_multiple_aspects_a: "はい。投稿を作成しているときに、アスペクトセレクターボタンを使用して、アスペクトを選択または選択を解除します。 「全てのアスペクト」がデフォルトの設定です。あなたの投稿は、選択したすべてのアスペクトに表示されます。また、サイドバーで投稿したいアスペクトを選択することもできます。投稿するときに左側のリストで選択したアスペクトは、新しい投稿の作成を始めるとき、自動的にアスペクトセレクターで選択されます。" post_multiple_aspects_q: "一度に複数のアスペクトに対してコンテンツを投稿することができますか?" - remove_notification_a: "いいえ。すでに彼らと共有しているとき、さらに多くのアスペクトに彼らを追加する場合も通知されません。" + remove_notification_a: "いいえ。すでに彼らとシェアしているとき、さらに多くのアスペクトに彼らを追加する場合も通知されません。" remove_notification_q: "誰かをあるアスペクトから、またはすべてのマイ アスペクトから削除した場合、これは彼らに通知されますか?" rename_aspect_a: "ストリームビューからサイドバーにある「マイ アスペクト」をクリックして、名前を変更したいアスペクトの横にある鉛筆アイコンをクリックするるか、連絡先のページに移動し、関連するアスペクトを選択します。次に、このページの一番上にあるアスペクト名の横にある編集アイコンをクリックして、名前を変更して「更新」を押します。" rename_aspect_q: "アスペクトの名前を変更する方法は?" @@ -393,7 +393,7 @@ ja: keyboard_shortcuts_li2: "k – 前の投稿へジャンプ" keyboard_shortcuts_li3: "c – 現在の投稿にコメント" keyboard_shortcuts_li4: "l – 現在の投稿にいいね!" - keyboard_shortcuts_li5: "r – 現在の投稿を再共有" + keyboard_shortcuts_li5: "r – 現在の投稿をリシェア" keyboard_shortcuts_li6: "m – 現在の投稿を展開" keyboard_shortcuts_li7: "o – 現在の投稿内の最初のリンクを開く" keyboard_shortcuts_li8: "Ctrl+Enter – 書いているメッセージを送信" @@ -421,8 +421,8 @@ ja: subscribe_feed_q: "誰かの公開の投稿をフィードリーダーで購読することはできますか?" title: "その他" pods: - find_people_a: "友達をダイアスポラ*に参加するように招待したい場合は、サイドバーの招待リンクまたはメールリンクを使用します。#タグ をフォローして、あなたが興味のあるものを共有する他の人を発見して 、アスペクトにその興味があるものの投稿者を追加します。公開の投稿で #初めて と叫んでください。" - find_people_q: "ポッドに参加したばかりです。どのように共有する人を見つけることができますか?" + find_people_a: "友達をダイアスポラ*に参加するように招待したい場合は、サイドバーの招待リンクまたはメールリンクを使用します。#タグ をフォローして、あなたが興味のあるものをシェアする他の人を発見して 、アスペクトにその興味があるものの投稿者を追加します。公開の投稿で #初めて と叫んでください。" + find_people_q: "ポッドに参加したばかりです。どのようにシェアする人を見つけることができますか?" title: "ポッド" use_search_box_a: "あなたが彼らの完全なダイアスポラ* ID (例 username@podname.org) を知っている場合は、それを検索して見つけることができます。あなたが同じポッド上にいる場合は、ユーザー名だけで検索することができます。別の方法としては、彼らのプロフィール名 (画面上に表示される名前) によって検索することができます。検索が最初に動作しない場合は、もう一度やり直してください。" use_search_box_q: "検索ボックスを使用して特定の個人を見つける方法は?" @@ -430,12 +430,12 @@ ja: what_is_a_pod_q: "ポッドとは何ですか?" posts_and_posting: char_limit_services_a: "投稿を少ない文字数に制限する必要がある場合 (Twitter の場合は 140、Tumblr の場合は 1000)、およびそのサービスのアイコンがハイライトされているときには、使用するために残っている文字数が表示されます。投稿がその制限よりも長い場合でも、そのサービスに投稿することはできますが、それらのサービスとダイアスポラ*上の投稿へのリンクでテキストは切り捨てられます。" - char_limit_services_q: "少ない文字数の提携サービスで投稿を共有している場合は?" + char_limit_services_q: "少ない文字数の提携サービスで投稿をシェアしている場合は?" character_limit_a: "65,535文字。Twitter よりも 65,395 文字以上多いです! ;)" character_limit_q: "投稿の文字数制限はいくつですか?" embed_multimedia_a: "投稿の中に、普通に URL (例. http://www.youtube.com/watch?v=nnnnnnnnnnn ) を貼り付けるだけで、ビデオやオーディオが自動的に埋め込まれます。サポートされているサイトは次のようなものがあります: YouTube、Vimeo、SoundCloud、Flickr、など。ダイアスポラ*は、この機能のためにoEmbedを使用しています。私たちは、常により多くのメディアソースをサポートしています。常に、シンプルに、完全なリンクで投稿することを忘れないでください - 短縮リンクではなく、ベースURLの後のオペレーターでもなく - プレビューを見るため、投稿後にページを更新する前に少し時間を置きます。" embed_multimedia_q: "投稿にビデオ、オーディオ、または他のマルチメディアコンテンツを埋め込む方法は?" - format_text_a: "%{markdown} と呼ばれる簡略化したシステムを使用することによって。全てのマークダウン構文を %{here} で参照することができます。共有する前に、あなたのメッセージがどのように見えるかを参照することができるので、プレビューボタンはここでは本当に役に立ちます。" + format_text_a: "%{markdown} と呼ばれる簡略化したシステムを使用することによって。全てのマークダウン構文を %{here} で参照することができます。シェアする前に、あなたのメッセージがどのように見えるかを参照することができるので、プレビューボタンはここでは本当に役に立ちます。" format_text_q: "自分の投稿内のテキストの書式 (太字、斜体など) を設定する方法は?" hide_posts_a: "投稿の先頭にマウスを移動すると、X が右側に表示されます。それをクリックすると、投稿を非表示にして、それに関する通知をミュートします。それを投稿した人のプロフィールページを訪問すると、まだ投稿を見ることができます。" hide_posts_q: "投稿を非表示にする方法は?" @@ -457,38 +457,38 @@ ja: size_of_images_a: "いいえ。ストリームまたはシングルポストビューに合わせて画像は自動的にリサイズされます。 マークダウン記法は、画像のサイズを指定するためのコードを持っていません。" size_of_images_q: "投稿やコメント内の画像のサイズをカスタマイズすることはできますか?" stream_full_of_posts_a1: "あなたのストリームは、3 種類の投稿で構成されています:" - stream_full_of_posts_li1: "あなたと共有している人々による投稿は 2 種類あります: 公開の投稿と、あなたが含まれるアスペクトと共有した限定公開の投稿。これらの投稿をあなたのストリームから削除するには、単にその人との共有を止めます。" + stream_full_of_posts_li1: "あなたとシェアしている人々による投稿は 2 種類あります: 公開の投稿と、あなたが含まれるアスペクトとシェアした限定公開の投稿。これらの投稿をあなたのストリームから削除するには、単にその人とのシェアを止めます。" stream_full_of_posts_li2: "あなたがフォローしているタグを含む公開の投稿。これらを削除するには、そのタグのフォローを止めます。" stream_full_of_posts_li3: "コミュニティスポットライトに記載されている人々による公開の投稿。これらは設定のアカウントタブで、オプション「ストリームにコミュニティスポットライトを表示?」のチェックをオフにすることによって除外することができます。" - stream_full_of_posts_q: "なぜ私のストリームが、知らない人や共有していない人からの投稿でいっぱいなのですか?" + stream_full_of_posts_q: "なぜ私のストリームが、知らない人やシェアしていない人からの投稿でいっぱいなのですか?" title: "投稿" private_posts: can_comment_a: "非公開の投稿を行う前にそのアスペクトに配置されていた、ログインしているダイアスポラ*ユーザーのみがそれにコメントまたはいいね!することができます。" can_comment_q: "非公開の投稿に、誰がコメントまたはいいね!することができますか?" - can_reshare_a: "誰もいません。非公開の投稿は再共有できません。しかし、そのアスペクト内のログインしているダイアスポラ*ユーザーは、潜在的にコピーおよび貼り付けることができます。それらの人々を信頼するかどうかはあなた次第です!" - can_reshare_q: "誰が私の非公開の投稿を再共有することができますか?" - see_comment_a: "投稿を共有された人 (元の投稿者によって選択されたアスペクトにいる人) のみ、そのコメントやいいね!を見ることができます。 " + can_reshare_a: "誰もいません。非公開の投稿はリシェアできません。しかし、そのアスペクト内のログインしているダイアスポラ*ユーザーは、潜在的にコピーおよび貼り付けることができます。それらの人々を信頼するかどうかはあなた次第です!" + can_reshare_q: "誰が私の非公開の投稿をリシェアすることができますか?" + see_comment_a: "投稿をシェアされた人 (元の投稿者によって選択されたアスペクトにいる人) のみ、そのコメントやいいね!を見ることができます。 " see_comment_q: "私が非公開の投稿にコメントまたはいいね!したとき、誰がそれを見ることができますか?" title: "非公開の投稿" who_sees_post_a: "非公開の投稿を行う前にそのアスペクトに配置されていた、ログインしているダイアスポラ*ユーザーのみがそれを見ることができます。" who_sees_post_q: "アスペクトにメッセージを投稿すると (例 非公開の投稿)、誰がそれを見ることができますか?" private_profiles: title: "非公開プロフィール" - whats_in_profile_a: "すべてのセクションを完了している場合、あなたの非公開プロフィールには、略歴、場所、性別、誕生日が含まれています。この情報はすべてオプションです - それを提供するかどうかはあなた次第です。あなたのアスペクトに追加したログインユーザーだけが、あなたの非公開プロフィールを見ることができます。彼らがあなたのプロフィールページにアクセスすると、彼らが含まれているアスペクトに対して行われた非公開の投稿も、あなたの公開の投稿に混じって表示されます。" + whats_in_profile_a: "すべてのセクションを完了している場合、あなたの非公開プロフィールには、略歴、所在地、性別、誕生日が含まれています。この情報はすべてオプションです - それを提供するかどうかはあなた次第です。あなたのアスペクトに追加したログインユーザーだけが、あなたの非公開プロフィールを見ることができます。彼らがあなたのプロフィールページにアクセスすると、彼らが含まれているアスペクトに対して行われた非公開の投稿も、あなたの公開の投稿に混じって表示されます。" whats_in_profile_q: "私の非公開プロフィールには何がありますか?" - who_sees_profile_a: "あなたと共有しているすべてのログインしたユーザー (あなたのアスペクトの一つに彼らを追加しているという意味)。しかし、あなたをフォローしていても、あなたがフォローしていない人は、あなたの公開情報が表示されるだけです。" + who_sees_profile_a: "あなたとシェアしているすべてのログインしたユーザー (あなたのアスペクトの一つに彼らを追加しているという意味)。しかし、あなたをフォローしていても、あなたがフォローしていない人は、あなたの公開情報が表示されるだけです。" who_sees_profile_q: "誰が私の非公開プロフィールを見ますか?" who_sees_updates_a: "あなたのアスペクトにいる誰もが、あなたの非公開プロフィールへの変更を見ています。 " who_sees_updates_q: "私の非公開プロフィールへの更新を誰が見ますか?" public_posts: - can_comment_reshare_like_a: "すべてのログインしているダイアスポラ*ユーザーがコメントしたり、再共有したり、いいね!をすることができます。" - can_comment_reshare_like_q: "誰が、私の公開の投稿をコメントしたり、再共有、いいね!することができますか?" + can_comment_reshare_like_a: "すべてのログインしているダイアスポラ*ユーザーがコメントしたり、リシェアしたり、いいね!をすることができます。" + can_comment_reshare_like_q: "誰が、私の公開の投稿をコメントしたり、リシェア、いいね!することができますか?" deselect_aspect_posting_a: "アスペクトの選択を解除しても、公開の投稿には影響を与えません。公開されて、あなたの連絡先のすべてのストリームに表示されます。特定のアスペクトにのみ投稿が見えるようにするには、公開者の下のアスペクトセレクターからそのアスペクトを選択する必要があります。" deselect_aspect_posting_q: "公開の投稿を行うときに、1 つまたは複数のアスペクトの選択を解除した場合はどうなりますか?" find_public_post_a: "あなたの公開の投稿は、あなたをフォローしている誰かのストリームに表示されます。あなたが公開の投稿に #タグ を含めた場合、そのタグをフォローしているすべての人が自分のストリームであなたの投稿を見つけます。すべての公開の投稿は、ログインしていない場合でも、誰でも閲覧することができる特定の URL も持っています - そのため、公開の投稿は Twitter、ブログ、などから直接リンクすることができます。公開の投稿は、検索エンジンにインデックスされることもできます。" find_public_post_q: "私の公開の投稿を他の人はどのように見つけますか?" - see_comment_reshare_like_a: "公開の投稿のコメント、いいね!、および再共有もまた、公開されます。すべてのログインしているダイアスポラ*ユーザーと、インターネット上の誰でも公開の投稿とあなたのやり取りを見ることができます。" - see_comment_reshare_like_q: "私が公開の投稿にコメントしたり、再共有、いいね!したとき、誰がそれを見ることができますか?" + see_comment_reshare_like_a: "公開の投稿のコメント、いいね!、およびリシェアもまた、公開されます。すべてのログインしているダイアスポラ*ユーザーと、インターネット上の誰でも公開の投稿とあなたのやり取りを見ることができます。" + see_comment_reshare_like_q: "私が公開の投稿にコメントしたり、リシェア、いいね!したとき、誰がそれを見ることができますか?" title: "公開の投稿" who_sees_post_a: "インターネットを使っている誰もがあなたが公開としてマークした投稿を参照する可能性があるので、あなたの投稿を本当に公開にしたいかを確認してください。世界に手を差し伸べる素晴らしい方法です。" who_sees_post_q: "公開で何かを投稿すると、誰がそれを見ることができますか?" @@ -503,32 +503,32 @@ ja: who_sees_updates_a: "あなたのプロフィールページにアクセスすると、誰でも更新を見ることができます。" who_sees_updates_q: "誰が私の公開プロフィールへの更新を見ていますか?" resharing_posts: - reshare_private_post_aspects_a: "いいえ。非公開の投稿を再共有することはできません。これが、特定のグループの人々とのみそれを共有した、元の投稿者の意向を尊重することです。" - reshare_private_post_aspects_q: "非公開の投稿を、選択したアスペクトに再共有することはできますか?" - reshare_public_post_aspects_a: "いいえ。公開の投稿を再共有すると、それは自動的にあなたの公開の投稿の一つになります。 それを特定のアスペクトと共有するには、新しい限定公開の投稿に投稿の内容をコピーおよび貼り付けします。" - reshare_public_post_aspects_q: "公開の投稿を、選択したアスペクトに再共有することはできますか?" - title: "投稿の再共有" + reshare_private_post_aspects_a: "いいえ。非公開の投稿をリシェアすることはできません。これが、特定のグループの人々とのみそれをシェアした、元の投稿者の意向を尊重することです。" + reshare_private_post_aspects_q: "非公開の投稿を、選択したアスペクトにリシェアすることはできますか?" + reshare_public_post_aspects_a: "いいえ。公開の投稿をリシェアすると、それは自動的にあなたの公開の投稿の一つになります。 それを特定のアスペクトとシェアするには、新しい限定公開の投稿に投稿の内容をコピーおよび貼り付けします。" + reshare_public_post_aspects_q: "公開の投稿を、選択したアスペクトにリシェアすることはできますか?" + title: "投稿のリシェア" sharing: add_to_aspect_a1: "エイミーがベンをアスペクトに追加し、ベンは (まだ) エイミーをアスペクトに追加していないとしましょう:" - add_to_aspect_a2: "これは非対称の共有として知られています。もし、ベンもまた、アスペクトにエイミーを追加すると、お互いのストリームにエイミーとベンの両方公開の投稿や関連する非公開の投稿が表示され、相互共有になるでしょう。そして、エイミーはベンの非公開プロフィールを表示することができるでしょう。そして、彼らはお互いに非公開メッセージを送信することができるでしょう。" - add_to_aspect_li1: "ベンは、エイミーがベンと「共有を開始した」という通知を受け取ります。" + add_to_aspect_a2: "これは非対称のシェアとして知られています。もし、ベンもまた、アスペクトにエイミーを追加すると、お互いのストリームにエイミーとベンの両方公開の投稿や関連する非公開の投稿が表示され、相互シェアになるでしょう。そして、エイミーはベンの非公開プロフィールを表示することができるでしょう。そして、彼らはお互いに非公開メッセージを送信することができるでしょう。" + add_to_aspect_li1: "ベンは、エイミーがベンと「シェアを開始した」という通知を受け取ります。" add_to_aspect_li2: "エイミーは彼女のストリームに、ベンの公開の投稿の表示が始まります。" add_to_aspect_li3: "エイミーは、ベンの非公開の投稿は表示されません。" add_to_aspect_li4: "ベンは、彼のストリームにエイミーの公開または非公開の投稿は表示されません。" add_to_aspect_li5: "ベンがエイミーのプロフィールページに移動した場合、彼には、エイミーが彼を配置しているアスペクトに行った非公開の投稿 (と同様に、誰でも見ることができる彼女の公開の投稿) が表示されます。" - add_to_aspect_li6: "ベンはエイミーの非公開プロフィール (略歴、場所、性別、誕生日) を見ることができます。" - add_to_aspect_li7: "エイミーは、ベンの連絡先ページの「自分だけに共有」の下に表示されます。" + add_to_aspect_li6: "ベンはエイミーの非公開プロフィール (略歴、所在地、性別、誕生日) を見ることができます。" + add_to_aspect_li7: "エイミーは、ベンの連絡先ページの「自分だけにシェア」の下に表示されます。" add_to_aspect_li8: "エイミーもベンを@メンションすることができるようになります。" add_to_aspect_q: "私のアスペクトの一つに誰かを追加すると、または誰かが彼らのアスペクトの一つに私を追加すると、どうなりますか?" - list_not_sharing_a: "いいえ。しかし、彼らのプロフィールページを訪問して、その人があなたと共有しているかどうかを確認することができます。その場合、彼らが配置したアスペクトを示すボタンが緑色になり、ない場合は、それが灰色になるでしょう。" + list_not_sharing_a: "いいえ。しかし、彼らのプロフィールページを訪問して、その人があなたとシェアしているかどうかを確認することができます。その場合、彼らが配置したアスペクトを示すボタンが緑色になり、ない場合は、それが灰色になるでしょう。" list_not_sharing_q: "私のアスペクトの一つに追加した人で、その人のアスペクトに私を追加していない人のリストはありますか?" - only_sharing_a: "これらは、自分のアスペクトの一つにあなたを追加した人ですが、あなたのアスペクトのいずれにも (まだ) いない人です。言い換えれば、彼らはあなたと共有していますが、あなたは彼らと共有していません: 彼らを、あなたを「フォローしている」人と考えることができます。あなたがアスペクトに彼らを追加した場合、その後、「自分だけに共有」ではなくアスペクトの下に表示されます。上記を参照してください。" - only_sharing_q: "私の連絡先ページの「自分だけに共有」の下にリストされている人は誰ですか?" + only_sharing_a: "これらは、自分のアスペクトの一つにあなたを追加した人ですが、あなたのアスペクトのいずれにも (まだ) いない人です。言い換えれば、彼らはあなたとシェアしていますが、あなたは彼らとシェアしていません: 彼らを、あなたを「フォローしている」人と考えることができます。あなたがアスペクトに彼らを追加した場合、その後、「自分だけにシェア」ではなくアスペクトの下に表示されます。上記を参照してください。" + only_sharing_q: "私の連絡先ページの「自分だけにシェア」の下にリストされている人は誰ですか?" see_old_posts_a: "いいえ。彼らは、そのアスペクトへの新しい投稿のみを見ることができます。彼ら (および他の誰でも) あなたのプロフィールページであなたの古い公開の投稿を見ることができ、また、彼らのストリームでもそれらを見ることができます。" see_old_posts_q: "私が誰かをアスペクトに追加すると、彼らは私がすでにそのアスペクトに投稿している古い投稿を見ることができますか?" - sharing_notification_a: "誰かがあなたと共有を始めるたびに、あなたは通知を受信するはずです。" - sharing_notification_q: "誰かが私と共有を始めたとき、どのように私がそれを知りますか?" - title: "共有" + sharing_notification_a: "誰かがあなたとシェアを始めるたびに、あなたは通知を受信するはずです。" + sharing_notification_q: "誰かが私とシェアを始めたとき、どのように私がそれを知りますか?" + title: "シェア" tags: filter_tags_a: "これは、まだ直接ダイアスポラ*を通して利用できませんが、いくつかの %{third_party_tools} がこれを提供するために作成されています。" filter_tags_q: "私のストリームからいくつかのタグをフィルター/除外することはできますか?" @@ -549,13 +549,13 @@ ja: home: default: be_who_you_want_to_be: "あなたがなりたい人になる" - be_who_you_want_to_be_info: "多くのネットワークは、あなたが実名を使用することを主張しています。ダイアスポラ*はそうではありません。ここでは、あなたがなりたい人を選んで、自分自身についてたくさん、あるいは少しだけ、あなたが望むように共有することができます。あなたが他の人と対話する方法は、本当にあなた次第です。" + be_who_you_want_to_be_info: "多くのネットワークは、あなたが実名を使用することを主張しています。ダイアスポラ*はそうではありません。ここでは、あなたがなりたい人を選んで、自分自身についてたくさん、あるいは少しだけ、あなたが望むようにシェアすることができます。あなたが他の人と対話する方法は、本当にあなた次第です。" byline: "あなたがコントロールしているオンラインソーシャルの世界" choose_your_audience: "観客を選択" - choose_your_audience_info: "ダイアスポラ*のアスペクトは、あなたが希望する人たちとだけ共有することができます。あなたが好きなように公開にも非公開にもすることができます。世界中と面白い写真を、または親しい友人とだけ深い秘密を共有します。あなたがコントロールします。" + choose_your_audience_info: "ダイアスポラ*のアスペクトは、あなたが希望する人たちとだけシェアすることができます。あなたが好きなように公開にも非公開にもすることができます。世界中と面白い写真を、または親しい友人とだけ深い秘密をシェアします。あなたがコントロールします。" headline: "%{pod_name}にようこそ" own_your_data: "あなた自身のデータを所有する" - own_your_data_info: "多くのネットワークはあなたのデータを使用して、あなたのやり取りを分析することでお金を稼ぎます。そして、この情報を使用して、物を宣伝します。ダイアスポラ*は、あなたが他のユーザーと接続して、共有することを可能にする以外の目的のために、あなたのデータを使用しません。" + own_your_data_info: "多くのネットワークはあなたのデータを使用して、あなたのやり取りを分析することでお金を稼ぎます。そして、この情報を使用して、物を宣伝します。ダイアスポラ*は、あなたが他のユーザーと接続して、シェアすることを可能にする以外の目的のために、あなたのデータを使用しません。" podmin: admin_panel: "管理者用パネル" byline: "インターネットを変更しようとしています。セットアップを始めますか?" @@ -595,7 +595,7 @@ ja: comma_separated_plz: "コンマ区切りで複数のメールアドレスを入力できます。" invite_someone_to_join: "知り合いをダイアスポラ*に招待しましょう!" language: "言語" - paste_link: "友達とこのリンクを共有してダイアスポラ*に招待するか、直接リンクをメールで送ります。" + paste_link: "友達とこのリンクをシェアしてダイアスポラ*に招待するか、直接リンクをメールで送ります。" send_an_invitation: "招待を送信する" sending_invitation: "招待を送信中..." layouts: @@ -642,7 +642,7 @@ ja: index: all_notifications: "すべての通知" also_commented: "コメントも" - and: "又は" + and: "および" and_others: few: "and %{count} others" many: "and %{count} others" @@ -652,17 +652,17 @@ ja: zero: "そして他にはいません" comment_on_post: "投稿にコメント" liked: "いいね!しました" - mark_all_as_read: "全件を既読にする" + mark_all_as_read: "すべて既読にする" mark_all_shown_as_read: "表示をすべて既読としてマーク" mark_read: "既読にする" mark_unread: "未読にする" mentioned: "メンションされました" no_notifications: "まだ通知は何もありません" notifications: "通知" - reshared: "再共有しました" - show_all: "全件表示" + reshared: "リシェアしました" + show_all: "すべて表示" show_unread: "未開封メッセージを表示" - started_sharing: "共有を開始しました" + started_sharing: "シェアを開始しました" liked: few: "%{actors} has just liked your %{post_link}." many: "%{actors} has just liked your %{post_link}." @@ -703,23 +703,23 @@ ja: few: "%{actors} has reshared your %{post_link}." many: "%{actors} has reshared your %{post_link}." one: "%{actors} has reshared your %{post_link}." - other: "%{actors}さんが投稿%{post_link}を再共有しました。" + other: "%{actors}さんが投稿%{post_link}をリシェアしました。" two: "%{actors} has reshared your %{post_link}." - zero: "%{actors}さんが投稿%{post_link}を再共有しました。" + zero: "%{actors}さんが投稿%{post_link}をリシェアしました。" reshared_post_deleted: few: "%{actors} reshared your deleted post." many: "%{actors} reshared your deleted post." one: "%{actors} reshared your deleted post." - other: "%{actors}さんが削除された投稿を再共有しました。" + other: "%{actors}さんが削除された投稿をリシェアしました。" two: "%{actors} reshared your deleted post." - zero: "%{actors}さんが削除された投稿を再共有しました。" + zero: "%{actors}さんが削除された投稿をリシェアしました。" started_sharing: few: "%{actors} started sharing with you." many: "%{actors} started sharing with you." one: "%{actors} started sharing with you." - other: "%{actors}さんが、あなたと共有を始めました。" + other: "%{actors}さんが、あなたとシェアを始めました。" two: "%{actors} started sharing with you." - zero: "%{actors}さんが、あなたと共有を始めました。" + zero: "%{actors}さんが、あなたとシェアを始めました。" notifier: a_limited_post_comment: "あなたが確認する、ダイアスポラ*の制限公開の投稿に新しいコメントがあります。" a_post_you_shared: "投稿" @@ -846,14 +846,14 @@ ja: comment: "コメント" post: "投稿" reshared: - reshared: "%{name}さんがあなたの投稿を再共有しました" + reshared: "%{name}さんがあなたの投稿をリシェアしました" view_post: "投稿を見る >" single_admin: admin: "ダイアスポラ*管理者" subject: "ダイアスポラ*アカウントの重要なお知らせ:" started_sharing: - sharing: "あなたへの共有を開始しています!" - subject: "%{name}さんがダイアスポラ*であなたと共有を始めました" + sharing: "あなたへのシェアを開始しています!" + subject: "%{name}さんがダイアスポラ*であなたとシェアを始めました" view_profile: "%{name}さんのプロフィールを見る" thanks: "ありがとうございます。" to_change_your_notification_settings: "通知設定を変更します" @@ -867,7 +867,7 @@ ja: looking_for: "タグ%{tag_link}の付いた投稿をお探しですか?" no_one_found: "…1人も見つかりませんでした。" no_results: "何かを検索しないといけません。" - results_for: "検索結果:" + results_for: "%{search_term} に一致するユーザー" search_handle: "友達を見つけるために彼らのダイアスポラ* ID (username@pod.tld) を使用していることを確認してください。" searching: "検索中です。もうしばらくお待ちください..." send_invite: "まだ見つかりませんか?招待を送信しましょう!" @@ -881,7 +881,7 @@ ja: show: closed_account: "このアカウントは削除されています" does_not_exist: "存在しない連絡先です!" - has_not_shared_with_you_yet: "%{name}さんはまだあなたにどの投稿も共有していません!" + has_not_shared_with_you_yet: "%{name}さんはまだあなたにどの投稿もシェアしていません!" photos: create: integrity_error: "写真のアップロードに失敗しました。確かに画像ファイルだったのでしょうか。" @@ -894,7 +894,7 @@ ja: invalid_ext: "{file}のファイル名は不正です。{extensions}以外の拡張子は使えません。" size_error: "{file}は大きすぎます。ファイルサイズの上限は{sizeLimit}です。" new_profile_photo: - upload: "新しいプロフィール写真をアップロードする!" + upload: "新しいプロフィール写真をアップロードしてください!" show: show_original_post: "元の投稿を表示する" polls: @@ -914,25 +914,25 @@ ja: other: "%{author}さんの写真%{count}枚" two: "Two photos by %{author}" zero: "%{author}さんの写真はありません" - reshare_by: "%{author}さんが再共有" + reshare_by: "%{author}さんがリシェア" privacy: "プライバシー" profile: "プロフィール" profiles: edit: - allow_search: "ダイアスポラ内の検索を許可します" - basic: "私の基本プロフィール" + allow_search: "ダイアスポラ*内の検索を許可する" + basic: "自分の基本プロフィール" basic_hint: "あなたのプロフィール内のすべての項目は省略可能です。あなたの基本的なプロフィールは、常に公開されます。" - extended: "私の拡張プロフィール" - extended_hint: "スイッチをクリックして、あなたの拡張プロフィールデータの表示を設定します。公開は、インターネットに表示されることを意味します。限定公開は、あなたと共有する人だけにこの情報が表示されることを意味します。" - extended_visibility_text: "あなたの拡張プロフィールの表示:" + extended: "自分の拡張プロフィール" + extended_hint: "スイッチをクリックして、あなたの拡張プロフィールデータの表示を設定します。公開は、インターネットに表示されることを意味します。限定公開は、あなたとシェアする人だけにこの情報が表示されることを意味します。" + extended_visibility_text: "拡張プロフィールを表示:" first_name: "名" last_name: "姓" limited: "限定公開" - nsfw_check: "私が共有するすべてのものを NSFW としてマーク" - nsfw_explanation: "NSFW (「閲覧注意」) は、仕事中に見るのに適しない可能性があるコンテンツの、ダイアスポラ*自主管理コミュニティ標準です。頻繁にこのような素材を共有する場合、それらを表示することを選択しない限り、あなたが共有するすべてのものが人々のストリームから表示されないように、このオプションをチェックしてください。" - nsfw_explanation2: "このオプションを選択しない場合、このような素材を共有するたびに #nsfw タグを追加してください。" + nsfw_check: "私がシェアするすべてのものを NSFW としてマーク" + nsfw_explanation: "NSFW (「閲覧注意」) は、仕事中に見るのに適しない可能性があるコンテンツの、ダイアスポラ*自主管理コミュニティ標準です。頻繁にこのような素材をシェアする場合、それらを表示することを選択しない限り、あなたがシェアするすべてのものが人々のストリームから表示されないように、このオプションをチェックしてください。" + nsfw_explanation2: "このオプションを選択しない場合、このような素材をシェアするたびに #nsfw タグを追加してください。" public: "公開" - settings: "プロフィールの設定" + settings: "プロフィール設定" update_profile: "プロフィール更新" your_bio: "略歴" your_birthday: "誕生日" @@ -963,14 +963,14 @@ ja: enter_email: "メールアドレスを入力してください。" enter_password: "パスワードを入力してください。" enter_password_again: "もう一度同じパスワードを入力してください。" - enter_username: "ユーザ名を選択してください。(半角英数字とアンダーバーのみ)" + enter_username: "ユーザー名を選択してください。 (半角英数字とアンダーバーのみ)" password: "パスワード" password_confirmation: "パスワードの確認" sign_up: "登録" submitting: "送信中..." terms: "アカウントを作成することによって、あなたは%{terms_link}に同意します。" terms_link: "利用規約" - username: "ユーザ名" + username: "ユーザー名" report: comment_label: "コメント:
%{data}" confirm_deletion: "項目を削除してもよろしいですか?" @@ -986,11 +986,11 @@ ja: failed: "何か問題があります" title: "報告の概要" reshares: - comment_email_subject: "%{author}の投稿の%{resharer}の再共有" + comment_email_subject: "%{author}の投稿の%{resharer}のリシェア" reshare: deleted: "元の投稿は作者によって削除されました。" - reshare_confirmation: "%{author}さんの投稿を再共有しますか?" - reshared_via: "...で再共有" + reshare_confirmation: "%{author}さんの投稿をリシェアしますか?" + reshared_via: "リシェア..." search: "検索" services: create: @@ -1006,12 +1006,12 @@ ja: connect: "接続" disconnect: "切断" edit_services: "サービスを編集する" - logged_in_as: "ログイン済みユーザ名:" + logged_in_as: "%{nickname}としてログインしています。" no_services_available: "このポッドで利用可能なサービスはありません。" not_logged_in: "現在ログインしていません。" really_disconnect: "%{service}から切断しますか。" - services_explanation: "サードパーティの共有サービスに接続すると、ダイアスポラ*に書きながらそれらに投稿を公開することができます。" - share_to: "%{provider}に共有" + services_explanation: "サードパーティのシェアサービスに接続すると、ダイアスポラ*に書きながらそれらに投稿を公開することができます。" + share_to: "%{provider}にシェア" title: "提携サービスを管理する" provider: facebook: "Facebook" @@ -1032,17 +1032,17 @@ ja: zero: "%{count} のアスペクトで" invitations: by_email: "メールで" - invite_your_friends: "知り合いを検索する" + invite_your_friends: "友達を招待する" invites: "招待" - share_this: "このリンクをメールやブログ、お気に入りのSNSで共有しましょう!" + share_this: "このリンクをメールやブログ、お気に入りのSNSでシェアしましょう!" public_explain: atom_feed: "Atom フィード" control_your_audience: "観客をコントロール" logged_in: "%{service}へログインしました。" manage: "提携サービスを管理する" - new_user_welcome_message: "あなたの投稿を分類し、あなたが興味があるものを共有する人を見つけるために、#ハッシュタグ を使用します。 @メンション で素晴らしい人々を呼び出します" + new_user_welcome_message: "あなたの投稿を分類し、あなたが興味があるものをシェアする人を見つけるために、#ハッシュタグ を使用します。 @メンション で素晴らしい人々に呼びかけます" outside: "公開投稿はダイアスポラ外の人にも表示されます。" - share: "共有" + share: "シェア" title: "提携サービスを設定する" visibility_dropdown: "このドロップダウンを使用して、投稿の表示を変更します。 (この最初の1つを公開することをご提案します。)" publisher: @@ -1057,8 +1057,8 @@ ja: poll: add_a_poll: "投票を追加" posting: "投稿中" - remove_location: "場所を削除" - share: "共有" + remove_location: "所在地を削除" + share: "シェア" upload_photos: "写真をアップロード" whats_on_your_mind: "いま何を考えている?" stream_element: @@ -1135,7 +1135,7 @@ ja: tagged_people: other: "%{count} 人が %{tag} でタグ付けしています" zero: "誰も %{tag} でタグ付けしていません" - username: "ユーザ名" + username: "ユーザー名" users: confirm_email: email_confirmed: "メール %{email} を有効にしました" @@ -1146,14 +1146,14 @@ ja: wrong_password: "パスワードが一致しません。" edit: also_commented: "他の人も連絡先の投稿にコメントしたとき" - auto_follow_aspect: "あなたが自動的に共有するユーザーのアスペクト:" - auto_follow_back: "あなたと共有を始めたユーザーと、自動的に共有" + auto_follow_aspect: "あなたが自動的にシェアするユーザーのアスペクト:" + auto_follow_back: "あなたとシェアを始めたユーザーと、自動的にシェア" change: "変更" change_color_theme: "色のテーマを変更" - change_email: "Change E-Mail" + change_email: "メールを変更" change_language: "言語変更" change_password: "パスワード変更" - character_minimum_expl: "少なくとも6字以上でなければいけません。" + character_minimum_expl: "6字以上にする必要があります。" close_account: dont_go: "行かないでください!" lock_username: "ユーザー名はロックされます。このポッド上で、同じIDのアカウントを作ることはできません。" @@ -1175,7 +1175,7 @@ ja: export_photos_in_progress: "現在、写真を処理しています。しばらくしてから、戻って確認してください。" following: "フォロー設定" last_exported_at: "(最終更新 %{timestamp})" - liked: "誰かがあなたの投稿をいいね!しました" + liked: "あなたの投稿をいいね!したとき" mentioned: "投稿に自分がメンションされたとき" new_password: "新しいパスワード" private_message: "非公開メッセージが届いたとき" @@ -1184,15 +1184,15 @@ ja: request_export_photos: "写真をリクエスト" request_export_photos_update: "写真を更新" request_export_update: "マイ プロフィールのデータを更新" - reshared: "誰かがあなたの投稿を再共有しました" + reshared: "あなたの投稿をリシェアしたとき" show_community_spotlight: "ストリームに「コミュニティスポットライト」を表示" show_getting_started: "「はじめに」のヒントを表示" someone_reported: "誰かが報告を送信" - started_sharing: "誰かがあなたと共有を始めました" + started_sharing: "あなたとシェアを始めたとき" stream_preferences: "ストリーム設定" your_email: "メールアドレス" your_email_private: "あなたのメールアドレスは、他のユーザーが見ることはありません" - your_handle: "ダイアスポラのユーザ名" + your_handle: "ダイアスポラ* ID" getting_started: awesome_take_me_to_diaspora: "素晴らしい!私をダイアスポラ*に連れてって" community_welcome: "あなたに加わっていただいて、ダイアスポラ*のコミュニティは幸せです!" @@ -1210,7 +1210,7 @@ ja: strip_exif: "アップロードした画像から場所、作成者、カメラモデルなどのメタデータを除去する (推奨)" title: "プライバシー設定" public: - does_not_exist: "ユーザ名「%{username}」は存在しません。" + does_not_exist: "ユーザー %{username} は存在しません!" update: color_theme_changed: "色のテーマを正常に変更しました。" color_theme_not_changed: "色のテーマを変更中にエラーが発生しました。" diff --git a/config/locales/diaspora/oc.yml b/config/locales/diaspora/oc.yml new file mode 100644 index 000000000..54f2c1098 --- /dev/null +++ b/config/locales/diaspora/oc.yml @@ -0,0 +1,1098 @@ +# Copyright (c) 2010-2013, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + + + +oc: + _applications: "Aplicacions" + _contacts: "Contactes" + _help: "Ajuda" + _services: "Servicis" + _statistics: "Estatisticas" + _terms: "Condicions d'utilizacion" + account: "Compte" + activerecord: + errors: + models: + contact: + attributes: + person_id: + taken: "deu èsser unic entre los contactes d'aqueste utilizaire." + person: + attributes: + diaspora_handle: + taken: "es ja pres." + poll: + attributes: + poll_answers: + not_enough_poll_answers: "I a pas pro de causidas possiblas" + poll_participation: + attributes: + poll: + already_participated: "Avètz ja paticipat a aquel sondatge !" + request: + attributes: + from_id: + taken: "es un doblon d'una requèsta qu'existís ja." + reshare: + attributes: + root_guid: + taken: "Es tròp crane non ? Avètz ja partejat aquesta publicacion !" + user: + attributes: + email: + taken: "es ja utilizat." + person: + invalid: "es invalid." + username: + invalid: "es invalid. Autorizam pas que las letras, chifras e caractèrs de soslinhament." + taken: "es ja pres." + admins: + admin_bar: + dashboard: "Tablèu de bòrd" + pages: "Paginas" + pod_network: "Ret del pod" + pod_stats: "Estats del Pod" + report: "Senhalaments" + sidekiq_monitor: "Monitor Sidekiq" + user_search: "Cercar un utilizaire" + weekly_user_stats: "Estatisticas d'utilizaire setmanièras" + dashboard: + fetching_diaspora_version: "A determinar la darrièra version de diaspora*..." + pod_status: "Estat del pod" + pods: + pod_network: "Ret del pod" + stats: + 2weeks: "2 setmanas" + 50_most: "Las 50 etiquetas mai popularas" + comments: + one: "%{count} comentari" + other: "%{count} comentaris" + zero: "Pas cap de comentari" + current_segment: "La categoria considerada a una mejana de %{post_yest} messatges per utilizaire, dempuèi %{post_day}" + daily: "Quotidiana" + display_results: "Afichatge dels resultats pel periòde %{segment}" + go: "Executar" + month: "Mes" + posts: + one: "%{count} publicacion" + other: "%{count} publicacions" + zero: "Pas cap de publicacion" + shares: + one: "%{count} partiment" + other: "%{count} partiments" + zero: "Pas cap de partiment" + tag_name: "Nom del tag : %{name_tag} Nombre : %{count_tag}" + usage_statistic: "Estatisticas d'utilizacion" + users: + one: "%{count} utilizaire" + other: "%{count} utilizaires" + zero: "Pas cap d'utilizaire" + week: "Setmana" + user_entry: + account_closed: "Compte tampat" + diaspora_handle: "identificant diaspora*" + email: "E-mail" + guid: "GUID" + id: "ID" + invite_token: "Geton de convit" + last_seen: "Darrièra connexion" + ? "no" + : Non + nsfw: "#nsfw" + unknown: "Desconegut" + ? "yes" + : Òc + user_search: + account_closing_scheduled: "La tampadura del compte %{name} es planificada. Serà efectuada dins gaire de temps." + account_locking_scheduled: "Lo compte de %{name} va èsser verrolhat dins un momenton" + account_unlocking_scheduled: "Lo compte de %{name} va èsser desverrolhat dins un momenton." + add_invites: "Apondre de convits" + are_you_sure: "Sètz segur que volètz tampar aqueste compte ?" + are_you_sure_lock_account: "Sètz segur que volètz verrolhar aqueste compte ?" + are_you_sure_unlock_account: "Sètz segur que volètz desverrolhar aqueste compte ?" + close_account: "Tampar aqueste compte" + email_to: "Adreça electronica de la persona de convidar" + invite: "Convidar" + lock_account: "Verrolhar lo compte" + under_13: "Mostrar los utilizaires qu'an mens de 13 ans (COPPA)" + unlock_account: "Desverrolhar lo compte" + users: + one: "%{count} utilizaire trobat" + other: "%{count} utilizaires trobats" + zero: "Cap d'utilizaire pas trobat" + view_profile: "Afichar lo perfil" + you_currently: + one: "Vos demòra actualament un convit %{link}" + other: "Vos demòran actualament %{count} convits %{link}" + zero: "Vos demòra pas mai de convit %{link} actualament" + weekly_user_stats: + amount_of: + one: "%{count} novèl utilizaire aquesta setmana" + other: "Nombre de novèls utilizaires aquesta setmana : %{count}" + zero: "Pas cap de novèl utilizaire aquesta setmana " + current_server: "La data actuala del servidor es %{date}" + all_aspects: "Totes los aspèctes" + api: + openid_connect: + authorizations: + destroy: + fail: "La temptativa de revocacion de l'autorizacion amb ID %{id} a fracassat" + new: + access: "%{name} a besonh d'accedir a :" + approve: "Aprovar" + bad_request: "ID client mancant o URL de redireccion" + client_id_not_found: "Pas de client amb client_id %{client_id} amb URL de redireccion %{redirect_uri} pas trobat" + deny: "Regetar" + no_requirement: "%{name} a pas besonh de permissions" + redirection_message: "Sètz segur que volètz donar accès a %{redirect_uri} ?" + error_page: + contact_developer: "Deuriatz contactar lo desvolopaire de l'aplicacion e inclure lo messatge d'error detalhat seguent :" + could_not_authorize: "L'aplicacion a pas pugut èsser autorizada" + login_required: "D'en primièr, vos cal vos connectar per fin de poder autorizar aquesta aplicacion" + title: "Ops ! Quicòm a trucat :(" + scopes: + name: + name: "nom" + nickname: + description: "Aquò dina las permissions ligadas a l'escais a l'aplicacion" + name: "Escais" + openid: + description: "Aquò permet a l'aplicacion de veire vòstre perfil basic" + name: "perfil basic" + picture: + description: "Aquò dona las permissions ligadas als imatges a l'aplicacion" + name: "imatge" + profile: + description: "Aquò autpriza l'aplicacion a legir vòstre perfil complet" + name: "perfil complet" + read: + name: "veire perfil, flux e conversacions" + sub: + description: "Aquò autreja de sospermissions a l'aplicacion" + name: "jos" + write: + description: "Aquò permet a l'aplicacion de mandar de publicacions novèlas, escriure de conversacions e publicar de reaccions." + name: "mandar publicacions, conversacions e reaccions" + user_applications: + index: + access: "%{name} a accès a :" + edit_applications: "Aplicacions" + no_requirement: "%{name} as pas besonh d'autorizacions." + title: "Aplicacions autorizadas" + no_applications: "Avètz pas d'aplicacions autorizadas" + policy: "Veire la politica de confidencialitat de l'aplicacion" + revoke_autorization: "Revocar" + tos: "Veire las condicions d'utilizacion de l'aplicacion" + are_you_sure: "Sètz segur ?" + are_you_sure_delete_account: "Sètz segur que volètz tampar vòstre compte ? Aquò se pòt pas anullar !" + aspect_memberships: + destroy: + failure: "La persona a pas pogut èsser levada de l'aspècte amb succès." + forbidden: "Sètz pas autorizat a far aquò" + invalid_statement: "Impossible de duplicar l'enregistrament." + no_membership: "Se pòt pas trobar la persona seleccionada dins aqueste aspècte." + success: "La persona es estada levada de l'aspècte amb succès." + aspects: + add_to_aspect: + failure: "Fracàs de l'apondon del contacte a l'aspècte." + success: "Lo contacte es ben estat apondut a l'aspècte." + aspect_listings: + add_an_aspect: "+ Apondre un aspècte" + aspect_stream: + make_something: "Fasètz quicòm" + stay_updated: "Demoratz al fial" + stay_updated_explanation: "Vòstre flux principal conten totes vòstres contactes, etiquetas seguidas, e publicacions de qualques sòcis creatius de la comunautat." + destroy: + failure: "%{name} a pas pogut èsser suprimit." + success: "%{name} es ben estat levat." + edit: + aspect_list_is_not_visible: "Los contactes dins aqueste aspècte se pòdon veire entre eles." + aspect_list_is_visible: "Los contactes dins aqueste aspècte se pòdon veire entre eles." + confirm_remove_aspect: "Sètz segur de volètz suprimir aqueste aspècte ?" + rename: "Renomenar" + update: "Metre a jorn" + updating: "En cors de mesa a jorn" + index: + donate: "Far un don" + help: + any_problem: "Avètz agut un problèma ?" + contact_podmin: "Contactar ladministrator de vòstre pod !" + do_you: "Volètz :" + feature_suggestion: "... aver una suggestion de %{link} ?" + find_a_bug: "... trobar %{link} ?" + have_a_question: "... aver una %{link} ?" + here_to_help: "La comunautat diaspora* es aquí !" + mail_podmin: "Corrièl del podmin" + need_help: "Besonh d'ajuda ?" + tag_bug: "bug" + tag_feature: "suggestion" + tag_question: "question" + tutorial_link_text: "Tutorials" + tutorials_and_wiki: "%{faq}, %{tutorial} e %{wiki} : vos ajudan per vòstres primièrs passes." + introduce_yourself: "Es vòstre flux. Rejonhètz-nos e presentatz-vos." + keep_pod_running: "Permetètz a %{pod} de foncionar rapidament e ofrissètz una dòsi de cafè mesadièra a nòstres servidors !" + new_here: + follow: "Seguissètz %{link} e aculhissètz los novèls utilizaires de diaspora* !" + learn_more: "Ne saber mai" + title: "Aculhir los novèls venguts" + services: + content: "Podètz connectar aqueles servicis a diaspora* :" + heading: "Connectar de servicis" + welcome_to_diaspora: "Benvenguda dins diaspora*, %{name} !" + no_contacts_message: + community_spotlight: "Actualitat de la comunautat" + invite_link_text: "convidar" + or_spotlight: "O podètz partejar amb %{link}" + try_adding_some_more_contacts: "Podètz recercar o %{invite_link} mai de contactes." + you_should_add_some_more_contacts: "Vos cal apondre un pauc mai de contactes !" + seed: + acquaintances: "Coneissenças" + family: "Familha" + friends: "Amics" + work: "Trabalh" + update: + failure: "Lo nom del vòstre aspecte, %{name}, es tròp long, enregistrament impossible." + success: "La modificacion de vòstre aspècte %{name}, a capitat." + blocks: + create: + failure: "Ai pas pogut ignorar aqueste utilizaire. #evasion" + success: "D'acòrdi, tornaretz pas veire aqueste utilizaire dins vòstre flux. #silenci !" + destroy: + failure: "Ai pas pogut quitar d'ignorar aqueste utilizaire. #evasion" + success: "Vejam çò qu'an a dire ! #digatzbonjorn" + bookmarklet: + explanation: "Publicatz sus diaspora* dempuèi oont que siá en apondent %{link} a vòstres marcapaginas." + heading: "Publicacion rapida" + post_something: "Publicatz sus diaspora*" + cancel: "Anullar" + comments: + new_comment: + comment: "Comentar" + commenting: "Comentari en cors de mandadís..." + contacts: + index: + add_a_new_aspect: "Apondre un aspècte novèl" + add_contact: "Apondre un contacte" + all_contacts: "Totes los contactes" + community_spotlight: "Actualitat de la comunautat" + my_contacts: "Mos contactes" + no_contacts: "Sembla que vos cal apondre mai de contactes !" + no_contacts_message: "Consultatz %{community_spotlight}" + only_sharing_with_me: "Solament partejat amb ieu" + start_a_conversation: "Començar una conversacion" + title: "Contactes" + user_search: "Cercar un contacte" + spotlight: + community_spotlight: "Actualitat de la comunitat" + no_members: "I a pas encara cap de membre." + suggest_member: "Suggerir un membre" + conversations: + create: + fail: "Messatge invalid" + no_contact: "Ei, vos cal ajustar lo contacte en primièr !" + sent: "Messatge mandat" + destroy: + delete_success: "Conversacion escafada amb succès" + hide_success: "Conversacion amagada amb succès" + index: + conversations_inbox: "Discussions - Bóstia de recepcion" + inbox: "Bóstia de recepcion" + new_conversation: "Novèla conversacion" + no_messages: "Pas cap de messatges" + new: + message: "Messatge" + send: "Mandar" + sending: "Mandadís..." + subject: "Subjècte" + subject_default: "Pas de subjècte" + to: "Per" + new_conversation: + fail: "Messatge invalid" + show: + delete: "Suprimir la conversacion" + hide: "amagar e metre en mut la conversacion" + last_message: "Darrièr messatge recebut %{timeago}" + reply: "Respondre" + replying: "Responsa..." + date: + formats: + birthday: "%d de %B" + birthday_with_year: "%d de %B de %Y" + fullmonth_day: "%d de %B" + delete: "Suprimir" + email: "Corrièl" + error_messages: + helper: + correct_the_following_errors_and_try_again: "Corregissètz las errors seguentas, e tornatz ensajar." + fill_me_out: "Escriure aicí" + find_people: "Recercar de personas o de #tags" + help: + account_and_data_management: + close_account_q: "Cossí pòdi suprimir mon compte ?" + data_other_podmins_q: "Los administrators dels autres pods pòdon veire mas informacions ?" + data_visible_to_podmin_q: "Quina quantitat de mas informacions l'administrator del pod pòt veire ?" + download_data_q: "Pòdi telecargar una còpia de totas las donadas contengudas dins mon compte ?" + move_pods_q: "Cossí desplaçar mon compte d'un pod a un autre ?" + title: "Gestion del compte e de las donadas" + aspects: + contacts_know_aspect_a: "Non pas. Pòdon pas veire lo nom de l'aspècte qualqu'arribe." + contacts_know_aspect_q: "Mos contactes pòdon saber sus quin aspècte los ai mes ?" + contacts_visible_q: "Qué significa « rendre los contactes dins aqueste aspècte visibles entre eles » ?" + delete_aspect_q: "Cossí pòdi suprimir un aspècte ?" + person_multiple_aspects_q: "Pòdi apondre una persona a mantun aspècte ?" + post_multiple_aspects_q: "Pòdi mandar un messatge a mantun aspècte a l'encòp ?" + remove_notification_q: "Se suprimissi qualqu'un d'un de mos aspèctes, o totas las personas d'un aspècte, son prevengudas ?" + rename_aspect_a: "Clicatz \"Mos aspèctes\" sul costat esquèrra de la pagina de flux e clicatz sul gredonèl \"editar\" sus la dreita de l'aspècte de renomenar. Podètz tanben anar sus la pagina \"Contactes\" e seleccionar l'aspècte concernit. Clicatz alara sus l'icòna \"editar\" al costat del nom amont de la pagina, cambiatz lo nom e clicatz \"Metre a jorn\"." + rename_aspect_q: "Pòdi tornar nommar un aspècte ?" + restrict_posts_i_see_q: "Pòdi afichar unicament los messatges de certans aspèctes ?" + title: "Aspèctes" + what_is_an_aspect_q: "De qué es un aspècte ?" + who_sees_post_q: "Quand publiqui sus un aspècte, qual pòt o veire ?" + chat: + add_contact_roster_q: "Cossí discutir amb qialqu'un dins diaspora* ?" + contacts_page: "pagina dels contactes" + title: "Chat" + faq: "FAQ" + foundation_website: "site internet de la fondacion diaspora*" + getting_help: + get_support_a_faq: "Legir la pagina %{faq} de nòstre wiki" + get_support_a_hashtag: "Pausatz una question dins un messatge public sus diaspora* en utilizant l'hashtag %{question}" + get_support_a_irc: "Rejonhètz-nos sus %{irc} (Chat instantanèu)" + get_support_a_tutorials: "Referissètz-vos a nòstres %{tutorials}" + get_support_a_website: "Visitatz nòstre %{link}" + get_support_a_wiki: "Recercatz dins lo %{link}" + get_support_q: "De qué far se ma question a pas de responsa dins la FAQ ? A quines autres endreits pòdi obténer de documentcion ?" + getting_started_q: "Ajudatz-me ! Ai besonh d'ajuda per debutar !" + title: "Obténer d'ajuda" + getting_started_tutorial: "Seria de tutorials \"Mos primièrs passes\"" + here: "aicí" + irc: "IRC" + keyboard_shortcuts: + keyboard_shortcuts_a1: "Dins lo flux, podètz utilizar los acorchis de clavièr seguents :" + keyboard_shortcuts_li1: "j - Anar al messatge seguent" + keyboard_shortcuts_li2: "k - Anar al messatge precedent" + keyboard_shortcuts_li3: "c - Comentar lo messatge corrent" + keyboard_shortcuts_li4: "l - Marcat lo messatge corrent coma m'agradant" + keyboard_shortcuts_li5: "r - Repartejar aqueste messatge" + keyboard_shortcuts_li6: "m - Afichar l'ensemble del messatge" + keyboard_shortcuts_li7: "o - Dobrir lo primièr ligam d'aqueste messatge" + keyboard_shortcuts_li8: "ctrl + entrada - Mandar lo messatge en cors de redaccion" + keyboard_shortcuts_q: "Quines son los acorchis de clavièr existents ?" + title: "Acorchis de clavièr" + markdown: "Markdown" + mentions: + how_to_mention_q: "Cossí pòdi mencionar qualqu'un quand escrivi un messatge ?" + mention_in_comment_a: "Non pas, pel moment." + mention_in_comment_q: "Pòdi mencionar qualqu'un dins un comentari ?" + see_mentions_a: "Òc, clicatz sus \"@Mencions\" dins la colomna d'esquèrra de vòstra pagina d'acuèlh" + see_mentions_q: "I a un biais de veire las publicacions que soi mencionat dedins ?" + title: "Mencions" + what_is_a_mention_a: "Una mencion es un ligam cap al perfil d'una persona qu'apareis sus una publicacion. Quand qualqu'un es mencionat, receb una notificacion per atirar lor atencion sus la publicacion." + what_is_a_mention_q: "De qué es una “mencion” ?" + miscellaneous: + diaspora_app_q: "Existís una aplicacion diaspora* per Android o iOS ?" + photo_albums_q: "I a d'albums de fòtos o de vidèos ?" + subscribe_feed_q: "Me pòdi inscriure als messatges publics d'una persona amb un lector de fluxes ?" + title: "Divèrs" + pods: + find_people_q: "Veni de rejónher un pod, cossí pòdi trobar de monde per partejar amb eles ?" + title: "Pods" + use_search_box_q: "Cossí me cal utilizar lo camp de recèrca per trobar qualqu'un en particular ?" + what_is_a_pod_q: "Qu'es aquò un pod ?" + posts_and_posting: + char_limit_services_q: "Quin es lo limit de caractèrs pels messatges partejats amb un servici qu'a un limit de caractèrs mai pichon ?" + character_limit_a: "65 535 caractèrs. Son 65 395 caractèrs de mai que Twitter ! :)" + character_limit_q: "Qual es lo limit de caractèrs per las publicacions ?" + format_text_q: "Cossí pòdi formatar lo tèxte dins as publicacions (gras, italic, etc.) ?" + hide_posts_q: "Cossí escondi una publicacion ?" + image_text: "tèxte de l'imatge" + image_url: "url de l'imatge" + insert_images_comments_a1: "Podètz pas mandar d'imatges dins los comentaris mas lo còdi Markdown seguent" + insert_images_comments_a2: "aquò pòt èsser utiizat per inserir d'imatges a partir del web dins los comentaris o los messatges." + insert_images_comments_q: "Pòdi inserir d'imatges dins un comentari ?" + insert_images_q: "Cossí pòdi inserir d'imatges dins un messatge ?" + post_location_q: "Cossí pòdi apondre ma posicion a un messatge ?" + post_notification_q: "Cossí pòdi activar o desactivar las notificacions per un messatge ?" + post_poll_q: "Cossí pòdi apondre un sondatge a mon messatge ?" + post_report_q: "Cossí pòdi senhalar un messatge ofensant ?" + size_of_images_q: "Pòdi melhorar la talha dels imatges dins los messatges o los comentaris ?" + stream_full_of_posts_a1: "Vòstre flux es condtituit de tres tipes de messatges :" + stream_full_of_posts_li2: "Los messatges publics contenon un dels tags que seguissètz. Per los suprimir, daissatz de seguir lo tag." + stream_full_of_posts_q: "Perqué mon flux es plen de messatges que provenon de monde que coneissi pas e amb las qualas partegi pas ?" + title: "Publicacions e publicar" + private_posts: + can_comment_q: "Qual pòt comentar o a qual pòt agradar un messatge privat ?" + can_reshare_q: "Qual pòt repartejar mos messatges privats ?" + see_comment_q: "Quand comenti un messatge privat o que marqui que m'agrada , qual o pòt veire ?" + title: "Publicacions privadas" + who_sees_post_q: "Quand mandi un messatge a un aspècte (es a dire un messatge privat), qual o pòt veire ?" + private_profiles: + title: "Perfils privats" + whats_in_profile_q: "Qu'es aquò mon perfil privat ?" + who_sees_profile_q: "Qual pòt veire mon perfil privat ?" + who_sees_updates_a: "Qual que siá dins vòstres aspèctes pòt veire las modificacions de vòstre perdil privat." + who_sees_updates_q: "Qual pòt veire las mesas a jorn de mon perfil privat ?" + public_posts: + can_comment_reshare_like_a: "Quin utilizaire connectat a diaspora* que siá, pòt comentar, repartejar vòstre messatge public o marcar que li agrada." + can_comment_reshare_like_q: "Qual pòt comentar, repartejar, o marcar que li agrada mon messatge public ?" + find_public_post_q: "Cossí las autras personas pòdon trobar mos messatges publics ?" + title: "Publicacions publicas" + who_sees_post_q: "Quand publiqui quicòm publicament, qual pòt veire ?" + public_profiles: + title: "Perfils publics" + what_do_tags_do_q: "De qué fan los tags sus mon perfil public ?" + whats_in_profile_q: "Qué i a sul meu perfil public ?" + who_sees_profile_q: "Qual vei mon perfil public ?" + who_sees_updates_a: "Qual que siá pòt veire aqueles cambiaments en visitant vòstra pagina de perfil." + who_sees_updates_q: "Qual pòt veire las mesas a jorn de mon perfil public ?" + resharing_posts: + reshare_private_post_aspects_q: "Pòdi repartejar un messatge privat unicament amb certans aspèctes ?" + reshare_public_post_aspects_q: "Pòdi repartejar un messatge public unicament amb certans aspèctes ?" + title: "Repartejar los messatges." + sharing: + add_to_aspect_li2: "Amy començara de veire los messatges publics de Ben dins son flux." + add_to_aspect_li3: "Amy veirà pas cap dels messatges privats de Ben." + add_to_aspect_li6: "Ben serà autorizat a veire lo perfil privat d'Amy (bio, localizacion, sèxe, anniversari)." + add_to_aspect_li8: "Amy serà tanben capabla de @mencionar Ben dins un messatge." + sharing_notification_a: "Deuriatz recebre una notificacion a cada còp que qualqu'un comença de partejar amb vos." + sharing_notification_q: "Cossí pòdi saber quand qualqu'un comença de partejar amb ieu ?" + title: "Partiment" + tags: + filter_tags_q: "Cossí pòdi filtrar/exclure certans tags de mon flux ?" + followed_tags_q: "De qué son los \"#Tags Seguits\" e cossí pòdi seguir un tag ?" + people_tag_page_a: "I a monde qu'an listat aqueste tag per se descriure eles-meteisses dins lor perfil public." + people_tag_page_q: "Qualas son las personas listadas sus la partida d'esquèrra d'una pagina de tag ?" + tags_in_comments_q: "Pòdi inserir de tags dins un comentari o sonque dins de messatges ?" + title: "Etiquetas" + what_are_tags_for_q: "A qué servisson las etiquetas ?" + third_party_tools: "Aisinas tèrças" + title_header: "Ajuda" + tutorial: "tutorial" + tutorials: "tutorials" + wiki: "wiki" + home: + default: + be_who_you_want_to_be: "Siatz lo que volètz èsser" + byline: "La ret sociala ont gardatz lo contraròtle." + choose_your_audience: "Causissètz vòstre public" + headline: "Benvenguda sus %{pod_name}" + own_your_data: "Siatz proprietari de vòstras donadas" + podmin: + admin_panel: "panèl d'administrator" + byline: "Sètz a mand de cambiar la fàcia d'internet. Prèst a vos lançar ?" + configuration_info: "Dobrissètz %{database_path} e %{diaspora_path} dins vòstre editor de tèxte favorit e relegissètz-los menimosament, son comentats abondament." + configure_your_pod: "Configuratz vòstre pod" + contact_irc: "nos contactar sus IRC" + contribute: "Contribuissètz" + contribute_info: "Rendètz diaspora* encara melhor ! Se rencontratz de bugs, %{report_bugs} se vos plai." + create_an_account: "Creatz un compte" + create_an_account_info: "%{sign_up_link} per un novèl compte" + faq_for_podmins: "FAQ pels podmins sus nòstre wiki" + getting_help: "Obtenètz d'ajuda" + headline: "Benvenguda, l'amic." + make_yourself_an_admin: "Venètz administrator" + report_bugs: "raportatz-los" + update_instructions: "d'instruccions de mesa a jorn sul wiki de diaspora*" + update_your_pod: "Mantenètz vòstre pod a jorn" + update_your_pod_info: "Trobaretz %{update_instructions}." + invitation_codes: + not_valid: "Aqueste còdi es pas pus valid." + invitations: + a_facebook_user: "Un utilizaire de Facebook" + check_token: + not_found: "Geton de convit pas trobat" + create: + empty: "Mercés de picar almens una adreça de corrièl." + no_more: "Avètz pas mai de convits." + note_already_sent: "Las invitacions son ja estadas mandadas a : %{emails}" + rejected: "Las adreças de corrièr electronic seguentas an rencontrat un problèma : " + sent: "Los convits son estats mandats a %{emails}" + new: + codes_left: + one: "Pas mai qu'un convit de disponible amb aqueste còdi." + other: "Encara %{count} convits de disponibles amb aqueste còdi." + zero: "Pas mai de convit de disponible amb aqueste còdi." + comma_separated_plz: "Podètz entrar mantuna adreça de corrièl separadas per de virgulas." + invite_someone_to_join: "Convidar qualqu'un a rejónher diaspora* !" + language: "Lenga" + paste_link: "Partejatz aqueste ligam amb vòstres amics per los convidar dins diaspora*, o mandatz-lor aqueste ligam dirèctament per corrièl." + send_an_invitation: "Mandar un convit" + sending_invitation: "Mandadís del convit..." + layouts: + application: + back_to_top: "Tornar amont" + be_excellent: "Siatz benvolent amb cadun !" + powered_by: "Propulsat per dispora*" + public_feed: "Flux diaspora* public per %{name}" + source_package: "Telecargatz lo còdi font" + statistics_link: "Estatisticas del pod" + toggle: "Activar/desactivar la version mobila" + whats_new: "Qué de nòu ?" + header: + code: "Còdi" + logout: "Desconnexion" + profile: "Perfil" + settings: "Paramètres" + toggle_navigation: "Afichar/amagar lo menú" + limited: "Limitat" + more: "Mai" + no_results: "Cap de resultat pas trobat" + notifications: + also_commented: + one: "%{actors} a tanben comentat %{post_link} de %{post_author}." + other: "%{actors} an tanben comentat %{post_link} de %{post_author}." + zero: "%{actors} a tanben comentat %{post_link} de %{post_author}." + also_commented_deleted: + one: "%{actors} a comentat una publicacion suprimida." + other: "%{actors} an comentat una publicacion suprimida." + zero: "%{actors} a comentat una publicacion suprimida." + comment_on_post: + one: "%{actors} a comentat vòstra publicacion %{post_link}." + other: "%{actors} an comentat vòstra publicacion %{post_link}." + zero: "%{actors} a comentat vòstra publicacion %{post_link}." + index: + all_notifications: "Totas las notificacions" + also_commented: "Tanben comentat" + and: "e" + and_others: + one: "e una persona mai" + other: "e %{count} personas mai" + zero: "e degun mai" + comment_on_post: "Comentari sus un de vòstres messatges" + liked: "Que m'agrada" + mark_all_as_read: "Tot marcar coma legit" + mark_all_shown_as_read: "Marcar tot coma legit" + mark_read: "Marcar coma legit" + mark_unread: "Marcar coma pas legit" + mentioned: "Mencionat" + no_notifications: "Avètz pas encara cap de notificacion." + notifications: "Notificacions" + reshared: "Repartejat" + show_all: "afichar tot" + show_unread: "afichar los que son pas legits" + started_sharing: "A començat de partejar" + liked: + one: "Vòstra publicacion %{post_link} agrada a %{actors} persona." + other: "Vòstra publicacion %{post_link} agrada a %{actors} personas." + zero: "Vòstra publicacion %{post_link} agrada a %{actors} persona." + liked_post_deleted: + one: "Vòstra publicacion suprimida a pas agradat %{actors} persona." + other: "Vòstra publicacion suprimida a pas agradat %{actors} personas." + zero: "Vòstra publicacion suprimida a pas agradat %{actors} persona." + mentioned: + one: "%{actors} vos a mencionat dins la publicacion %{post_link}." + other: "%{actors} vos an mencionat dins la publicacion %{post_link}." + zero: "%{actors} vos a mencionat dins la publicacion %{post_link}." + mentioned_deleted: + one: "%{actors} vos a mencionat sus una publicacion suprimida." + other: "%{actors} vos an mencionat sus una publicacion suprimida." + zero: "%{actors} vos a mencionat sus una publicacion suprimida." + post: "publicacion" + private_message: + one: "%{actors} vos a mandat un messatge." + other: "%{actors} vos an mandat un messatge." + zero: "%{actors} vos a mandat un messatge." + reshared: + one: "%{actors} a repartejat vòstra publicacion %{post_link}." + other: "%{actors} an repartejat vòstra publicacion %{post_link}." + zero: "%{actors} a repartejat vòstra publicacion %{post_link}." + reshared_post_deleted: + one: "%{actors} a tornat partejar vòstra publicacion suprimida." + other: "%{actors} an tornat partejar vòstra publicacion suprimida." + zero: "%{actors} a tornat partejar vòstra publicacion suprimida." + started_sharing: + one: "%{actors} a començat de partejar amb vos." + other: "%{actors} an començat de partejar amb vos." + zero: "%{actors} a començat de partejar amb vos." + notifier: + a_limited_post_comment: "Avètz un comentari novèl sus un messatge amb visibilitat limitada." + a_post_you_shared: "una publicacion." + also_commented: + limited_subject: "I a un comentari novèl sus un messatge qu'avètz comentat" + click_here: "Clicatz aicí" + comment_on_post: + limited_subject: "I a un comentari novèl sus una publicacion qu'avètz comentada" + reply: "Respondre o veire lo messatge de %{name} >" + confirm_email: + click_link: "Per activar vòstre novèla adreça de corrièl %{unconfirmed_email}, seguissètz aqueste ligam :" + subject: "Mercés d'activar vòstre novèl corrièl %{unconfirmed_email}" + email_sent_by_diaspora: "Aqueste corrièl es estat mandaat per %{pod_name}. Se volètz pas mai de corrièls atal," + export_email: + body: |- + Bonjorn %{name}, + + Vòstras donadas personalas son estadas tractadas e ara, las podètz telecargar en clicant sus [aqueste ligam](%{url}). + + Coralament, + + Lo messatgièr automatic de diaspora* + subject: "Vòstras donadas personalas son prèstas a èsser telecargadas, %{name}" + export_failure_email: + body: |- + Bonjorn %{name}, + + I a agut un problèma al moment del tractament de vòstras donadas personalas en vista d'un telecargament. + Mercé de tornar ensajar. + + O planhèm sincèrament. + + Lo messatgièr automatic de diaspora* + subject: "I a agut un problèma amb vòstras donadas, %{name}" + export_photos_email: + body: |- + Bonjorn %{name}, + + Vòstras fòtos son estadas preparadas e son prèstas a èsser telecargadas en seguissent sus [aqueste ligam](%{url}). + + Coralament, + + Lo messatgièr automatic de diaspora* + subject: "%{name}, vòstras fòtos son prèstas per èsser telecargadas." + export_photos_failure_email: + body: |- + Bonjorn %{name}, + + I a agut un problèma al moment del tractament de vòstras fòtos personalas en vista d'un telecargament. + Mercé de tornar ensajar. + + O planhèm sincèrament. + + Lo messatgièr automatic de diaspora* + subject: "I a agut un problèma amb vòstras fòtos, %{name}" + hello: "Adiu %{name} !" + invite: + message: |- + Bonjorn ! + + %{diaspora_id} vos a convidat a rejónher diaspora* ! + + Clicatz sul ligam per començar + + %{invite_url}[1] + + O podètz apondre %{diaspora_id} amb vòstres contactes se avètz ja un compte. + + Amistats, + + Lo messatgièr automatic de diaspora* ! + + P.S : En cas que coneisseriatz pas (encara) çò qu'es diapora*, aicí[2] i a la responsa ! + + [1] : %{invite_url} + [2] : %{diasporafoundation_url} + invited_you: "%{name} vos a convidat sus diaspora*" + liked: + liked: "a %{name}, li a agradat vòstra publicacion" + limited_post: "A %{name}, li a agradat vòstra publicacion restrenta" + view_post: "Veire la publicacion >" + mentioned: + limited_post: "Sètz estat mencionat dins una publicacion restenta." + subject: "%{name} vos a mencionat sus diaspora*" + private_message: + reply_to_or_view: "Respondre o afichar aquela conversacion >" + subject: "Avètz un novèl messatge privat" + remove_old_user: + body: |- + Bonjorn, + + En rason de l'inactivitat de vòstre compte diaspora* %{pod_url}, planhèm de vos informar que lo sistèma l'a identificat coma devent èsser suprimit automaticament. Aquesta procedura se desenclava aprèp un periòde d'inactivitat superior a %{after_days} jorns. + + Podètz evitar la pèrda de vòstre compte en vos connectant a aqueste abans %{remove_after}, dins aqueste cas la procedura de supression serà automaticament anullada. + + Aquesta mantenença es efectuada per assegurar als utilizaires actius lo mehor foncionament possible d'aquesta instància de diaspora*. Vos mercejam de vòstra compreneson. + + Se volètz conservar vòstre compte, identificatz-vos aicí : + %{login_url} + + Esperam vos reveire lèu ! + + Lo messatgièr automatic de diaspora* + subject: "Vòstre compte diaspora* es estat senhalat coma devent èsser suprimit en rason de son inactivitat" + report_email: + body: |- + Bonjorn, + + lo %{type} amb l'identificant %{id} es estat marcat coma ofensant. + + [%{url}][1] + + Mercé de lo verificar tre que possible ! + + + Coralament, + + Lo messatgièr automatic de diaspora* + + [1]: %{url} + subject: "Un novèl %{type} es estat marcat coma ofensant" + type: + comment: "comentari" + post: "messatge" + reshared: + reshared: "%{name} a tornat partejar vòstra publicacion" + view_post: "Veire la publicacion >" + single_admin: + admin: "Vòstre administrator diaspora*" + subject: "Un messatge subre vòstre compte diaspora*" + started_sharing: + sharing: "a començat a partejar amb vos !" + subject: "%{name} a començat a partejar amb vos sus diaspora*" + view_profile: "Veire lo perfil de %{name}" + thanks: "Mercé," + to_change_your_notification_settings: "per cambiar vòstres reglatges de notificacion" + nsfw: "NSFW" + ok: "D'acòrdi" + people: + add_contact: + invited_by: "Sètz estat convidat per" + index: + couldnt_find_them: "Los trobatz pas ?" + looking_for: "Recercar publicacions marcadas %{tag_link}" + no_one_found: "... e pas cap trobat." + no_results: "E ben ! Vos cal cercar quicòm." + results_for: "Utilizaires que correspondon a %{search_term}" + searching: "Recèrca en cors, mercés d'esperar..." + send_invite: "Pas res encara ? Mandatz-lor un convit !" + person: + thats_you: "Sètz vosautres !" + profile_sidebar: + bio: "Biografia" + born: "Anniversari" + gender: "Sèxe" + location: "Localizacion" + show: + closed_account: "Aqueste compte es estat tampat" + does_not_exist: "Aquesta persona existís pas !" + has_not_shared_with_you_yet: "%{name} a pas encara partejar cap de publicacions amb vos !" + photos: + create: + integrity_error: "Fracàs al moment de mandar l'imatge.  Sètz segur qu'èra un imatge ?" + runtime_error: "Fracàs al moment de mandar l'imatge.  Sètz segur que vòstre cinta de seguretat es estacada ?" + type_error: "Fracàs al moment de mandar l'imatge.  Sètz segur qu'un imatge es estat apondut ?" + destroy: + notice: "Fòto suprimida" + new_photo: + empty: "{file} es voida, mercé de tornar seleccionar de fichièrs sens aquel d'aquí." + invalid_ext: "L'extension de {file} es pas valida. Sonque {extensions} son autorizadas." + size_error: "{file} es tròp granda, la talha maximala es {sizeLimit}." + new_profile_photo: + upload: "Mandatz una novèla fòto de perfil !" + show: + show_original_post: "Mostrar la publicacion originala" + polls: + votes: + one: "%{count} vòte pel moment" + other: "%{count} vòtes pel moment" + zero: "%{count} vòte pel moment" + posts: + presenter: + title: "Una publicacion de %{name}" + show: + forbidden: "Sètz pas autorizat a far aquò" + location: "Publicat dempuèi : %{location}" + photos_by: + one: "Una fòto per %{author}" + other: "%{author} fòtos per %{count}" + zero: "Pas cap de fòto per %{author}" + reshare_by: "Repartejat per %{author}" + privacy: "Vida privada" + profile: "Perfil" + profiles: + edit: + allow_search: "Autorizar lo monde a vos recercar dins diaspora*" + basic: "Mon perfil basic" + basic_hint: "Totes los elements de vòstre perfil son opcionals. Vòstre perfil basic serà totjorn visible publicament." + extended: "Mon perfil espandit" + extended_visibility_text: "Visibilitat de vòstre perfil complet." + first_name: "Pichon nom" + last_name: "Nom d'ostal" + limited: "Limitat" + nsfw_check: "Amagar tot çò que pòsti coma #NSFW" + public: "Public" + settings: "Paramètres del perfil" + update_profile: "Actualizar lo perfil" + your_bio: "Vòstra biografia" + your_birthday: "Vòstre anniversari" + your_gender: "Vòstre sèxe" + your_location: "Vòstra localizacion" + your_name: "Vòstre nom" + your_photo: "Vòstra fòto" + your_tags: "Descrivètz-vos en 5 mots" + your_tags_placeholder: "Per exemple #filme #catons #viatjar #professor #tolosa" + update: + failed: "Fracàs de l'actualizacion del perfil" + updated: "Perfil a jorn" + public: "Public" + reactions: + one: "1 reaccion" + other: "%{count} reaccions" + zero: "Pas de reaccion" + registrations: + closed: "Las inscripcions son tampadas sus aqueste pod diaspora*." + create: + success: "Avètz rejunt diaspora*!" + invalid_invite: "Lo ligam de convit donat es pas mai valid !" + new: + email: "Adreça electronica" + enter_email: "Picatz vòstra adreça de corrièl" + enter_password: "Picatz un senhal (sièis caractèrs minimum)" + enter_password_again: "Picatz lo meteis senhal coma de per abans" + enter_username: "Picatz un escais (sonque letras, chifras e jonhent bas _)" + password: "Senhal" + password_confirmation: "Confirmacion del senhal" + sign_up: "Marcatz-vos" + submitting: "Mandadís en cors..." + terms: "En creant un compte, acceptatz los %{terms_link}" + terms_link: "condicions d'utilizacion" + username: "Nom d'utilizaire" + report: + comment_label: "Comentari:
%{data}" + confirm_deletion: "Sètz segur que volètz suprimir aqueste element ?" + delete_link: "Suprimir l'element" + post_label: "Publicacion: %{title}" + reason_label: "Rason: %{text}" + reported_label: "Senhalat per %{person}" + reported_user_details: "Detalhs sus l'utilizaire senhalat" + review_link: "Marcat coma revist." + status: + destroyed: "Lo messatge es estat destruit" + failed: "I a agut un problèma." + title: "Vista d'ensemble dels senhalaments" + reshares: + comment_email_subject: "Partiment per %{resharer} d'un messatge de %{author}" + reshare: + deleted: "La publicacion originala foguèt suprimida per son autor." + reshare_confirmation: "Volètz tornar partejar la publicacion de %{author} ?" + reshared_via: "Repartejat per" + search: "Recercar" + services: + create: + already_authorized: "Un utilizaire que l'identificant diaspora*%{service_name} es %{diaspora_id} a ja autorizat aqueste compte %{service_name}." + failure: "Fracàs de l'autentificacion." + read_only_access: "Accès en lectura sola, ensajatz tornamai d'autorizar pus tard" + success: "Autentificacion capitada" + destroy: + success: "Autentificacion suprimida." + failure: + error: "Una error s'est produita al moment de la connexion amb aqueste servici" + index: + connect: "Connectar" + disconnect: "Se desconnectar" + edit_services: "Modificar los servicis" + logged_in_as: "Connectat coma %{nickname}." + no_services_available: "I a pas cap de servici de disponible sus aqueste pod." + not_logged_in: "Actualament, sètz pas connectat." + really_disconnect: "Se desconnectar de %{service}" + services_explanation: "Se connectar a de servicis tèrç vos permet de publicar vòstras publicacions suls aqueles servicis quand n'escrivètz sus diaspora*." + share_to: "Partejar amb %{provider}" + title: "Geir los servicis connectats" + provider: + facebook: "Facebook" + tumblr: "Tumblr" + twitter: "Twitter" + wordpress: "WordPress" + settings: "Paramètres" + shared: + aspect_dropdown: + mobile_row_checked: "%{name} (suprimir)" + mobile_row_unchecked: "%{name} (apondre)" + toggle: + one: "Dins %{count} aspècte" + other: "Dins %{count} aspècte" + zero: "Apondre lo contacte" + invitations: + by_email: "Per corrièl" + invite_your_friends: "Convidatz vòstres amics" + invites: "Convits" + share_this: "Partejatz aqueste ligam via corrièl, blog, o malhums socials !" + public_explain: + atom_feed: "Flux Atom" + control_your_audience: "Contrarotlatz vòstra audiéncia" + logged_in: "Connectat a %{service}" + manage: "Gerir los servicis connectats" + new_user_welcome_message: "Utilizatz las #etiquetas per triar vòstras publicacions e per trobar de monde que partejan vòstres interèsses. Interpellatz de personas genialas amb las @Mencions" + outside: "Los messatges publics poiràn èsser vistes per totes, quitament en defòra de diaspora*." + share: "Partejatz" + title: "Metre en plaça de servicis connectats" + publisher: + discard_post: "Suprimir la publicacion" + formatWithMarkdown: "Utilizatz la sintaxi %{markdown_link} per metre en forma vòstre messatge" + get_location: "Apondre vòstra localizacion" + new_user_prefill: + hello: "Adiu mond, soi %{new_user_tag}. " + i_like: "M'interèssa %{tags}. " + invited_by: "Mercé pel convit, " + newhere: "novèlaicí" + poll: + add_a_poll: "Apondre un sondatge" + posting: "Publicacion..." + remove_location: "Suprimir l'emplaçament" + share: "Partejar" + upload_photos: "Mandar de fòtos" + whats_on_your_mind: "A qué pensatz ?" + stream_element: + via: "Via %{link}" + via_mobile: "Mandat dempuèi un mobil" + simple_captcha: + label: "Entratz lo còdi dins lo camp" + message: + default: "Lo còdi provesit correspond pas a l'imatge" + failed: "La verificacion antirobòt a fracassat" + user: "L'imatge e lo còdi son diferents" + placeholder: "Sasissètz lo contengut de l'imatge" + statistics: + active_users_halfyear: "Utilizaires actius aqueste semèstre" + active_users_monthly: "Utilizaires actius per mes" + closed: "Tampat" + disabled: "Indisponible" + enabled: "Disponible" + local_comments: "Comentaris locals" + local_posts: "Messatges locals" + name: "Nom" + network: "Ret" + open: "Dobrir" + registrations: "Enregistraments" + services: "Servicis" + total_users: "Total d'utilizaires" + version: "Version" + status_messages: + create: + success: "Mencion de : %{names}" + new: + mentioning: "Menciona : %{person}" + too_long: "Avisatz-vos d'escriure un messatge d'estatut de %{count} caractèrs maximum. Compta actualament %{current_length} caractèrs." + stream_helper: + no_more_posts: "Avètz atent la fin d'aqueste flux." + no_posts_yet: "I a pas encara cap de messatge aicí." + streams: + activity: + title: "Mon activitat" + aspects: + title: "Mos aspèctes" + aspects_stream: "Aspèctes" + comment_stream: + title: "Publicacions comentadas" + community_spotlight_stream: "Actualitat de la comunitat" + followed_tag: + add_a_tag: "Apondre una etiqueta" + follow: "Seguir" + title: "#Tags seguits" + followed_tags_stream: "#Tags seguits" + like_stream: + title: "Flux dels elements qu'agradan" + mentioned_stream: "@Mencions" + mentions: + title: "@Mencions" + multi: + title: "Flux" + public: + title: "Activitat publica" + tags: + title: "Messatges tagats : %{tags}" + tag_followings: + manage: + no_tags: "Seguissètz pas cap de tag." + title: "Gerir los tags seguits." + tags: + name_too_long: "Vòstre tag deu far mens de %{count} caractèrs ( actualament, %{current_length})" + show: + follow: "Seguir #%{tag}" + none: "L'etiqueta voida existís pas !" + stop_following: "Arrectar de seguir #%{tag}" + tagged_people: + one: "1 persona marcada amb %{tag}" + other: "%{count} personas marcadas amb %{tag}" + zero: "Degun es pas marcat amb %{tag}" + username: "Nom d'utilizaire" + users: + confirm_email: + email_confirmed: "Corrièl %{email} activat" + email_not_confirmed: "Es possible que lo corrièl no siá activat. Marrit ligam ?" + destroy: + no_password: "Mercé de picar vòstre sehnal actual per tampar lo vòstre compte." + success: "Vòstre compte es estat verrolhat. Pòt prene vint minuta per ne finalizar la tampadura. Mercés d'aver ensajat diaspora*." + wrong_password: "Lo senhal dintrat correpond pas amb lo senhal actual." + edit: + also_commented: "qualqu'un a comentat una publicacion qu'avètz comentada" + auto_follow_aspect: "Aspècte pels utilizaires seguts automaticament :" + auto_follow_back: "Automaticament partejar amb los utilizaires que partejan amb vos" + change: "Modificar" + change_color_theme: "Cambiar lo tèma de color" + change_email: "Cambiar de corrièl" + change_language: "Cambiar de lenga" + change_password: "Cambiar lo senhal" + character_minimum_expl: "deu conténer sièis caractèrs al minim" + close_account: + dont_go: "E, se vos plai, patiscatz pas !" + lock_username: "Vòstre nom d'utilizaire serà verrolhat. Poiretz pas crear un novèl compte sus aqueste pod amb lo meteis ID." + locked_out: "Seretz desconnectat e vòstre compte serà inaccessible fins a sa supression." + make_diaspora_better: "Nos agradariá fòrça que demorèssetz e nos ajudèssetz a far un melhor diaspora* puslèu que de nos daissar. Se volètz vertadièrament partir, pasmens, vaquí çò que se va passar :" + mr_wiggles: "Mossur Wiggles serà triste de vos veire partir." + no_turning_back: "Es pas possible de tornar en arrièr ! Se sètz segur de vos, sasissètz vòstre senhal çaijós." + close_account_text: "Tampar lo compte" + comment_on_post: "qualqu'un comenta vòstra publicacion" + current_password: "Senhal actual" + current_password_expl: "Lo senhal que causissètz..." + download_export: "Telecargar mon perfil" + download_export_photos: "Telecargar mas fòtos" + edit_account: "Modificar lo compte" + email_awaiting_confirmation: "Vos avèm mandat un ligam d'activacion a l'adreça %{unconfirmed_email}. Fins al moment qu'auretz sul ligam e activetz la novèla adreça, anam contunhar d'utilizar vòstra adreça d'origina %{email}." + export_data: "Exportar de donadas" + export_in_progress: "Sèm a tractar vòstras donadas. Verificatz tornamai l'avançament dins un moment." + export_photos_in_progress: "Sèm a tractar vòstras fòtos. Tornatz aicí dins un momenton." + following: "Paramètres de partiment" + last_exported_at: "(Darrièra mesa a jorn a %{timestamp})" + liked: "a qualqu'un li a agradat vòstra publicacion" + mentioned: "...se vos menciona dins un messatge." + new_password: "Senhal novèl" + private_message: "Avètz recebut un messatge privat." + receive_email_notifications: "Recebre una notificacion per corrièl quand :" + request_export: "Demandar mas donadas de perfil" + request_export_photos: "Demandar mas fòtos" + request_export_photos_update: "Refrescar mas fòtos." + request_export_update: "Refrescar mas donadas de perfil" + reshared: "qualqu'un a tornat partejar vòstra publicacion" + show_community_spotlight: "Mostrar l'actualitat de la comunitat dins lo flux" + show_getting_started: "Mostrar las astúcias de descoberta" + someone_reported: "Qualqu'un a senhalat un messatge" + started_sharing: "qualqu'un a començat a partejar amb vos" + stream_preferences: "Preferéncias del flux" + your_email: "Vòstre corrièl" + your_email_private: "Vòstre email serà pas jamai vist per d'autres utilizaires" + your_handle: "Vòstre ID diaspora*" + getting_started: + awesome_take_me_to_diaspora: "Òsca ! Mena-me cap a diaspora*" + community_welcome: "La comunautat diaspora* es contenta de vos aver a bòrd." + connect_to_facebook_link: "Connexion amb vòtre compte Facebook en cors" + hashtag_explanation: "Las etiquetas vos permeton de charrar e seguir vòstres interèsses. Son tanben una bona manièra de trobar de mond novèl sus diaspora*." + hashtag_suggestions: "Ensejatz etiquetas coma aquestas #art #filmes #gif, etc." + well_hello_there: "E ben, adiu monde !" + what_are_you_in_to: "Qué vos interèssa ?" + who_are_you: "Qual sètz ?" + privacy_settings: + ignored_users: "Utilizaires ignorats" + no_user_ignored_message: "Actualament, ignoratz pas cap d'utilizaire" + stop_ignoring: "Arrestar d'ignorar" + strip_exif: "Levar dels imatges mandats las metadonadas talas coma luòc de presa, autor o modèl d'aparelh fòto (recomandat)" + title: "Reglatges de confidencialitat" + public: + does_not_exist: "L'utilizaire %{username} existís pas !" + update: + color_theme_changed: "Tèma de color cambiat." + color_theme_not_changed: "Una error s'es produita pendent lo cambiament de tèma de color." + email_notifications_changed: "Notificacions corrièl cambiadas" + follow_settings_changed: "Reglatges cambiats pel seguiment" + follow_settings_not_changed: "Cambiament dels reglatges pel seguiment fracassat." + language_changed: "Lenga cambiada" + language_not_changed: "Cambiament de lenga fracassat" + password_changed: "Senhal cambiat.  Ara, vos podètz connectar amb lo senhal novèl." + password_not_changed: "Cambiament de senhal mancat" + settings_not_updated: "Mesa a jorn fracassada" + settings_updated: "Reglatges mes a jorn" + unconfirmed_email_changed: "Corrièl cambiat. Cal far una activacion." + unconfirmed_email_not_changed: "Cambiament de corrièl mancat" + will_paginate: + next_label: "seguent »" + previous_label: "« precedent" \ No newline at end of file diff --git a/config/locales/diaspora/ru.yml b/config/locales/diaspora/ru.yml index f56854595..a6c2a5758 100644 --- a/config/locales/diaspora/ru.yml +++ b/config/locales/diaspora/ru.yml @@ -761,15 +761,13 @@ ru: body: |- Привет, - Всвязи с неактивностью вашей учётной записи diaspora* на %{pod_url}, мы вынуждеы сообщить вам, что система отметила вашу учётную запись к автоматизированному удалению. Это происходит автоматически по прошествию периода неактивности более %{after_days} дней. + Похоже, Вы расхотели пользоваться учётной записью %{pod_url}, Вы не использовали её уже %{after_days} дней! Чтобы обеспечить активных пользователей нашего сервера максимальной производительностью, мы время от времени ищем и удаляем из нашей базы данных учётные записи, которые более не нужны своим владельцам. К сожалению, Ваша учётная запись попала в кандидаты на удаление. - Вы можете избежать потери учётной записи, зайдя в нее до %{remove_after}, в случае чего удаление будет автоматически отменено. + Однако, мы с удовольствием оставим Вашу учётную запись за Вами и не будем удалять её, если вы дадите знать нам об этом, просто зайдя к нам до %{remove_after}. А когда заглянете, заодно ещё разок осмотритесь - с Вашего последнего посещения у нас тут наверняка случилось немало интересного! А ещё можно осмотреть, что другие люди помечали #хэштэгами, которые встречаются в записях интересных Вам людей. - Эта техническая операция выполняется с целью убедиться в том, что активные пользователи получают большую часть ресурсов данной инстанции diaspora* . Благодарим за понимание. + Для Вашего удобства, вот ссылка на страницу входа: %{login_url}. Если Вы забыли свои реквизиты для входа, Вы можете восстановить их на этой странице. - Если вы хотите сохранить ваш аккаунт, пожалуйста, войдите в него здесь: %{login_url} - - В надежде увидеть вас снова, + В надежде увидеть Вас снова, Почтовый робот diaspora*! subject: "Ваша учётная запись помечена на удаление по причине неактивности" report_email: diff --git a/config/locales/diaspora/te.yml b/config/locales/diaspora/te.yml index 2bd38f1a1..f4deec27b 100644 --- a/config/locales/diaspora/te.yml +++ b/config/locales/diaspora/te.yml @@ -269,6 +269,7 @@ te: get_support_a_irc: "%{irc} (ప్రత్యక్ష సంభాషణ) లో మాతో చేరండి" get_support_a_tutorials: "మా %{tutorials}ను సందర్శించండి" get_support_a_website: "మా %{link}ను సందర్శించండి" + get_support_a_wiki: "%{link} ను వెతకండి" get_support_q: "నా ప్రశ్నకు ఈ ఎఫ్ఎక్యూలో సమాధానం లేకపోతే ఏంచేయాలి? ఇంకా ఎక్కడక్కడ నేను తోడ్పాటు పొందగలను?" getting_started_q: "సహాయం! ప్రారంభించడానికి నాకు కొంత ప్రాథమిక సహాయం కావాలి!" title: "సహాయం పొందడం" @@ -329,6 +330,7 @@ te: sharing: title: "పంచుకోవడం" tags: + followed_tags_q: "“#అనుసరించే కొసలు” అంటే ఏమిటి మరియు కొసను అనుసరించడం ఎలా?" title: "కొసలు" what_are_tags_for_q: "కొసలు ఎందుకోసం?" third_party_tools: "తృతీయ-పక్ష పనిముట్లు" @@ -458,13 +460,15 @@ te: message: |- హలో!    - మీరు డయాస్పోరా*లో చేరుటకు ఆహ్వానించబడ్డారు!  + మీరు డయాస్పోరా*లో చేరుటకు %{diaspora_id} చే ఆహ్వానించబడ్డారు!    ప్రారంభించుటకు ఈ లంకెను నొక్కండి    [%{invite_url}][1]    -   +  లేదా %{diaspora_id} ను మీ పరిచయాలకు చేర్చుకోవచ్చు ఒకవేళ మీకు ఇదివరకే ఖాతా ఉండుంటే. + + ప్రేమతో,    డయాస్పోరా* ఈమెయిలు యంత్రికుడు! @@ -491,6 +495,8 @@ te: %{id}తో ఉన్న %{type} ప్రమాదకరమైందిగా గుర్తించబడింది. + కారణం: %{reason} + [%{url}][1] దయచేసి ఎంత త్వరగా వీలైతే అంత తొందరగా సమీక్షించండి! @@ -498,7 +504,7 @@ te: సంతోషంతో, - డయాస్పోరా* ఈమెయిలు రోబోట్! + డయాస్పోరా* ఈమెయిలు యంత్రుడు! [1]: %{url} type: @@ -790,7 +796,7 @@ te: request_export_photos_update: "నా ఛాయాచిత్రాలను తాజాపరుచు" request_export_update: "నా ప్రవర దత్తాంశాన్ని తాజాపరుచు" reshared: "ఎవరో మీ టపాను మరలా పంచుకున్నారు" - show_community_spotlight: "ప్రవాహంలో సంఘపు స్పాట్‌లైట్‌ను చూపించు" + show_community_spotlight: "ప్రవాహంలో \"సంఘపు స్పాట్‌లైట్‌ను\" చూపించు" show_getting_started: "“ఆరంభించు” సూచనలను చూపించు" someone_reported: "ఎవరో ఒకరు నివేదిక పంపారు" started_sharing: "ఎవరో మీతో పంచుకోవడం మొదలుపెట్టారు" diff --git a/config/locales/javascript/javascript.cs.yml b/config/locales/javascript/javascript.cs.yml index ad78ec149..a80f67c44 100644 --- a/config/locales/javascript/javascript.cs.yml +++ b/config/locales/javascript/javascript.cs.yml @@ -8,11 +8,45 @@ cs: javascripts: admin: pods: + actions: "Akce" + added: "Přidáno" + check: "Provést test spojení" + follow_link: "otevřít odkaz v prohlížeči" + last_check: "poslední kontrola:" + more_info: "Zobrazit více informací" + ms: + few: "<%= count %>ms" + one: "<%= count %>ms" + other: "<%= count %>ms" + no_info: "Nyní nejsou dostupné žádné další informace" + not_available: "Není k dispozici" + offline_since: "Odpojený od doby:" + recheck: + failure: "Kontrola nebyla provedena" + response_time: "Doba odezvy:" + server_software: "Software serveru:" + ssl: "SSL" + ssl_disabled: "SSL vypnuto" + ssl_enabled: "SSL zapnuté" + states: + dns_failed: "Selhalo rozpoznání jména (DNS)" + http_failed: "Selhalo spojení HTTP" + net_failed: "Pokus o spojení selhal" + no_errors: "Budiž" + ssl_failed: "Selhalo bezpečné spojení (SSL)" + unchecked: "Neoznačené" + unknown_error: "Během kontroly se objevila nespecifikovaná chyba" + version_failed: "Nebylo možné zjistit verzi software" + status: "Stav" + unknown: "Neznámý" version_failed: few: "<%= count %> pody nemají verzi (staré pody, žádné NodeInfo)" one: "Jeden pod nemá verzi (starý pod, žádné NodeInfo)." other: "<%= count %> podů nemá verzi (staré pody, žádné NodeInfo)" zero: "Není pod, který by neměl verzi." + admins: + dashboard: + error: "Není možné zjistit poslední verzi diaspora*." and: "a" aspect_dropdown: add_to_aspect: "Přidat kontakt" @@ -92,6 +126,8 @@ cs: recent_notifications: "Poslední oznámení" search: "Hledat" settings: "Nastavení" + toggle_mobile: "Vybrat mobil" + toggle_navigation: "Vybrat navigaci" view_all: "Zobrazit vše" hide_post: "Skrýt tento příspěvek" hide_post_failed: "Není možné tento příspěvek skrýt" @@ -122,6 +158,11 @@ cs: looking_good: "Tedy, vypadáte úžasně!" size_error: "{file} je přiliš veliký, maximální velikost je {sizeLimit}." poll: + answer_count: + few: "<%=count%> hlasů" + one: "1 hlas" + other: "<%=count%> hlasů" + zero: "0 hlasů" close_result: "Skrýt výsledky" count: few: "zatím <%=count%> hlasy" @@ -146,6 +187,34 @@ cs: publisher: add_option: "Přidejte odpověď" limited: "Omezený — váš příspěvek bude přístupný pouze lidem, se kterými sdílíte" + markdown_editor: + preview: "Náhled" + texts: + code: "zde kód" + heading: "text záhlaví" + insert_image_description_text: "zde popište obrázek" + insert_image_help_text: "zde vložte odkaz na obrázek" + insert_image_title: "vložte titulek k obrázku" + insert_link_description_text: "zde popište odkaz" + insert_link_help_text: "zde vložte odkaz" + italic: "text kurzívou" + list: "zde textový seznam" + quote: "zde text v uvozovkách" + strong: "tučný text" + tooltips: + bold: "Tučně" + cancel: "Zrušit zprávu" + code: "Vložit kód" + heading: "Záhlaví" + insert_image: "Vložit obrázek" + insert_link: "Vložit odkaz" + insert_ordered_list: "Vložit tříděný seznam" + insert_unordered_list: "Vložit netříděný seznam" + italic: "Kurzíva" + preview: "Náhled zprávy" + quote: "Vložit uvozovky" + write: "Upravit zprávu" + write: "Zápis" near_from: "Odesláno z: <%= location %>" option: "Odpověď" public: "Veřejný — váš příspěvek si bude moci přečíst kdokoliv a může být nalezen vyhledávači" @@ -187,7 +256,12 @@ cs: other: "Zobrazit <%= count %> dalších komentářů" zero: "Zobrazit <%= count %> dalších komentářů" original_post_deleted: "Původní příspěvek byl odstraněn svým autorem." + permalink: "trvalý odkaz" public: "Veřejný" + reactions: + few: "<%= count%> reakcí" + one: "<%= count%> reakce" + other: "<%= count%> reakcí" reshare: "Sdílet" reshares: few: "<%= count %> uživatelé to sdíleli" @@ -215,6 +289,7 @@ cs: hour: "hodinou" hours: other: "%d hodinami" + inPast: "Jakýkoli moment" minute: "minutou" minutes: other: "%d minutami" diff --git a/config/locales/javascript/javascript.da.yml b/config/locales/javascript/javascript.da.yml index 6dc090be8..173731002 100644 --- a/config/locales/javascript/javascript.da.yml +++ b/config/locales/javascript/javascript.da.yml @@ -197,10 +197,16 @@ da: markdown_editor: preview: "Forhåndsvisning" texts: + code: "kode her" heading: "Overskriftstekst" + insert_image_description_text: "skriv billedbeskrivelse her" + insert_image_help_text: "indsæt billed-link her" + insert_image_title: "skriv billedtitel her" insert_link_description_text: "skriv link-beskrivelse her" insert_link_help_text: "Indsæt link her" italic: "tekst i kursiv" + list: "listetekst her" + quote: "citattekst her" strong: "fed tekst" tooltips: bold: "Fed" diff --git a/config/locales/javascript/javascript.hy.yml b/config/locales/javascript/javascript.hy.yml index 47b217e42..7bb2194bd 100644 --- a/config/locales/javascript/javascript.hy.yml +++ b/config/locales/javascript/javascript.hy.yml @@ -189,6 +189,34 @@ hy: publisher: add_option: "Պատասխան ավելացնել" limited: "Փակ. սա նշանակում է, որ գրառումդ տեսանելի է լինելու միայն այն մարդկանց, ում հետ կիսվում ես։" + markdown_editor: + preview: "Նախադիտել" + texts: + code: "կոդն այստեղ" + heading: "վերնագրի տեքստը" + insert_image_description_text: "նկարի նկարագրությունն այստեղ" + insert_image_help_text: "Տեղադրիր նկարի հղումն այստեղ՝" + insert_image_title: "նկարի անունն այստեղ" + insert_link_description_text: "հղման նկարագրությունն այստեղ" + insert_link_help_text: "Տեղադրիր հղումն այստեղ՝" + italic: "շեղատառ տեքստ" + list: "լցրու ցուցակն այստեղ" + quote: "մեջբերիր այստեղ" + strong: "թավատառ տեքստ" + tooltips: + bold: "Թավ" + cancel: "Չեղարկել" + code: "Կոդ ներմուծել" + heading: "Վերնագիր" + insert_image: "Նկար տեղադրել" + insert_link: "Հղում տեղադրել" + insert_ordered_list: "Կանոնավոր ցուցակ ներմուծել" + insert_unordered_list: "Անկանոն ցուցակ ներմուծել" + italic: "Շեղ" + preview: "Նախադիտել գրառումը" + quote: "Մեջբերում անել" + write: "Խմբագրել գրառումը" + write: "Շարադրել" near_from: "Գրառված է <%= location %>ից" option: "Պատասխան" public: "Հրապարակային. սա նշանակում է, որ գրառումդ տեսանելի է լինելու բոլորին և հասանելի կլինի փնտրող համակարգերի համար" diff --git a/config/locales/javascript/javascript.ia.yml b/config/locales/javascript/javascript.ia.yml index c59ff4789..783f19b22 100644 --- a/config/locales/javascript/javascript.ia.yml +++ b/config/locales/javascript/javascript.ia.yml @@ -192,7 +192,7 @@ ia: question: "Question" remove_post: "Remover iste entrata?" report: - name: "Reporto" + name: "Reportar" prompt: "Specifica un motivo:" prompt_default: "contento offensive" status: diff --git a/config/locales/javascript/javascript.ja.yml b/config/locales/javascript/javascript.ja.yml index e7897d2ea..b02294947 100644 --- a/config/locales/javascript/javascript.ja.yml +++ b/config/locales/javascript/javascript.ja.yml @@ -55,7 +55,7 @@ ja: aspect_dropdown: add_to_aspect: "Add to aspect" all_aspects: "全てのアスペクト" - error: "<%= name %>さんと共有を始めることができません。 無視しますか?" + error: "<%= name %>さんとシェアを始めることができません。 無視しますか?" error_remove: "アスペクトから<%= name %>さんを削除できませんでした :(" mobile_row_checked: "<%= name %> (削除)" mobile_row_unchecked: "<%= name %> (追加)" @@ -110,7 +110,7 @@ ja: failed_to_like: "いいね!に失敗しました。おそらく作者があなたを無視していませんか?" failed_to_post_message: "メッセージの投稿に失敗しました!" failed_to_remove: "エントリーの削除に失敗しました!" - failed_to_reshare: "再共有に失敗しました!" + failed_to_reshare: "リシェアに失敗しました!" getting_started: alright_ill_wait: "OK、私は待ちます。" hey: "Hey, <%= name %>!" @@ -149,8 +149,8 @@ ja: people: edit_my_profile: "マイ プロフィールを編集する" helper: - is_not_sharing: "<%= name %>さんはあなたと共有していません" - is_sharing: "<%= name %>さんはあなたと共有しています" + is_not_sharing: "<%= name %>さんはあなたとシェアしていません" + is_sharing: "<%= name %>さんはあなたとシェアしています" mention: "メンション" message: "メッセージ" not_found: "…1人も見つかりませんでした" @@ -180,13 +180,13 @@ ja: contacts: "連絡先" edit: "編集" gender: "性別" - location: "場所" + location: "所在地" photos: "写真" posts: "投稿" you_have_no_tags: "タグがありません!" publisher: add_option: "回答を追加" - limited: "限定公開 - 投稿はあなたが共有している人だけが見られるようになります" + limited: "限定公開 - 投稿はあなたがシェアしている人だけが見られるようになります" markdown_editor: preview: "プレビュー" texts: @@ -228,9 +228,9 @@ ja: created: "報告を正常に作成しました" exists: "報告はすでに存在します" reshares: - duplicate: "えっ?すでにこの投稿を共有しています!" - post: "<%= name %>さんの投稿を再共有しますか?" - successful: "投稿は正常に再共有されました!" + duplicate: "えっ?すでにこの投稿をリシェアしています!" + post: "<%= name %>さんの投稿をリシェアしますか?" + successful: "投稿は正常にリシェアされました!" show_more: "さらに表示する" stream: comment: "コメント" @@ -266,14 +266,14 @@ ja: reactions: other: "<%= count%> リアクション" zero: "<%= count%> リアクション" - reshare: "再共有" + reshare: "リシェア" reshares: few: "<%= count %> Reshares" many: "<%= count %> Reshares" one: "<%= count %> Reshare" - other: "<%= count %> 再共有" + other: "<%= count %> リシェア" two: "<%= count %> Reshares" - zero: "<%= count %> 再共有" + zero: "<%= count %> リシェア" show_nsfw_post: "投稿を表示する" show_nsfw_posts: "全て表示" tags: @@ -292,14 +292,14 @@ ja: day: "1日" days: other: "%d日" - hour: "大体1時間" + hour: "約1時間" hours: - other: "大体%d時間" + other: "約%d時間" inPast: "今さっき" minute: "約1分" minutes: other: "%d分" - month: "大体1ヶ月" + month: "約1ヶ月" months: other: "%dヶ月" prefixAgo: "" @@ -308,9 +308,9 @@ ja: suffixAgo: "前" suffixFromNow: "後" wordSeparator: " " - year: "大体1年" + year: "約1年" years: other: "%d年" unblock_failed: "このユーザーのブロック解除に失敗しました" viewer: - reshared: "再共有しました" \ No newline at end of file + reshared: "リシェアしました" \ No newline at end of file diff --git a/config/locales/javascript/javascript.nb.yml b/config/locales/javascript/javascript.nb.yml index 6367edcd9..ec8a4bae1 100644 --- a/config/locales/javascript/javascript.nb.yml +++ b/config/locales/javascript/javascript.nb.yml @@ -138,6 +138,33 @@ nb: publisher: add_option: "Legg til valg" limited: "Begrenset - posten din vil bare være synlig for de du deler med" + markdown_editor: + preview: "Forhåndsvis" + texts: + code: "kode her" + heading: "Tekst for overskrift" + insert_image_description_text: "legg til bildebeskrivelse her" + insert_image_help_text: "Sett inn bildelink her" + insert_image_title: "legg til bildetittel her" + insert_link_description_text: "legg til lenkebeskrivelse her" + insert_link_help_text: "sett inn lenke her" + italic: "Kursiv tekst" + quote: "sitert tekst her" + strong: "Uthevet tekst" + tooltips: + bold: "Fet skrift" + cancel: "Avbryt melding" + code: "Sett inn kode" + heading: "Overskrift" + insert_image: "Sett inn bilde" + insert_link: "Sett inn link" + insert_ordered_list: "Sett inn kronologisk liste" + insert_unordered_list: "Sett inn uordnet liste" + italic: "Kursiv" + preview: "Forhåndsvis melding" + quote: "Sett inn sitat" + write: "Rediger melding" + write: "Skriv" near_from: "Publisert fra: <%= location %>" option: "Valg <%= nr %>" public: "Offentlig - posten din vil være synlig for alle og kan bli funnet av søkemotorer" @@ -176,6 +203,7 @@ nb: one: "Vis <%= count %> kommentar til" other: "Vis <%= count %> komentarer til" zero: "Ikke flere kommentarer" + no_posts_yet: "Det er ingen poster å vise her ennå" original_post_deleted: "Den opprinnelige posten er slettet av forfatteren." public: "Offentlig" reshare: "Del" diff --git a/config/locales/javascript/javascript.oc.yml b/config/locales/javascript/javascript.oc.yml new file mode 100644 index 000000000..fc7dde386 --- /dev/null +++ b/config/locales/javascript/javascript.oc.yml @@ -0,0 +1,320 @@ +# Copyright (c) 2010-2013, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + + + +oc: + javascripts: + admin: + pods: + actions: "Accions" + added: "Apondut" + check: "executar un tèst de connexion" + errors: + one: "Lo tèst de connexion rapòrta una error per un pod." + other: "Lo tèst de connexion rapòrta una error per <%= count %> pods." + follow_link: "dobrir lo ligam dins lo navigador" + last_check: "darrièra verificacion :" + more_info: "mostrar mai d'informacion" + ms: + one: "<%= count %> ms" + other: "<%= count %> ms" + no_info: "Pas d informacion suplementària de disponibla pel moment." + not_available: "pas disponible" + offline_since: "fòra linha dempuèi :" + pod: "Pod" + recheck: + failure: "La verificacion es pas estada executada." + success: "Lo pod ven d'èsser verificat tornamai." + response_time: "Temps de responsa :" + server_software: "Logicial del servidor :" + ssl: "SSL" + ssl_disabled: "SSL desactivada" + ssl_enabled: "SSL activada" + states: + dns_failed: "La resolucion del nom (DNS) a fracassat" + http_failed: "La connexion HTTP a fracassat" + net_failed: "La tempativa de connexion a fracassat" + no_errors: "D'acòrdi" + ssl_failed: "La connexion securizada (SSL) a fracassat" + unchecked: "Pas verificat" + unknown_error: "Una error desconeguda s'es produita pendent la verificacion" + version_failed: "Impossible de determinar la version del programa" + status: "Estat" + unchecked: + one: "Demòra un pod qu'es pas brica estat verificat." + other: "Demòra <%= count %> pods que son pas brica estat verificats." + unknown: "desconegut" + version_failed: + one: "I a un pod que sa version es desconeguda (un ancian pod, pas de NodeInfo)." + other: "I a <%= count %> pods que lor versions son desconegudas (d'ancians pods, pas de NodeInfo)." + admins: + dashboard: + compare_versions: "La darrièra version de diaspora* es <%= latestVersion %>, vòstre pod vira jos la version <%= podVersion %>." + error: "Impossible de determinar la darrièra version de diaspora*." + outdated: "Vòstre pod es pas a jorn." + up_to_date: "Vòstre pod es a jorn !" + and: "e" + aspect_dropdown: + add_to_aspect: "Apondre un contacte" + all_aspects: "Totes los aspèctes" + error: "Impossible de partejar amb <%= name %>. Ignoratz aquesta persona ?" + error_remove: "Pòt pas suprimir <%= name %> de l'aspècte :(" + mobile_row_checked: "<%= name %> (suprimir)" + mobile_row_unchecked: "<%= name %> (apondre)" + select_aspects: "Seleccionar los aspèctes" + started_sharing_with: "Avètz començat de partejar amb <%= name %>." + stopped_sharing_with: "Avètz arrestat de partejar amb <%= name %>." + toggle: + one: "Dins <%= count %> aspècte" + other: "Dins <%= count %> aspèctes" + zero: "Dins <%= count %> aspècte" + updating: "mesa a jorn..." + aspect_navigation: + add_an_aspect: "+ Apondre un aspècte" + deselect_all: "Deseleccionat tot" + no_aspects: "Cap d'aspècte pas seleccionat" + select_all: "Seleccionar tot" + aspects: + create: + add_a_new_aspect: "Apondre un aspècte novèl" + failure: "La creacion de l'aspècte a fracassat." + success: "Vòstre novèl aspècte <%= name %> es estat creat" + make_aspect_list_visible: "Permetre als contactes d'aqueste aspècte de se veire entre eles ?" + name: "Nom" + bookmarklet: + post_something: "Publicar sus diaspora*" + post_submit: "Messatge en cors de publicacion..." + post_success: "Publicat ! Tampadura de la fenèstra popup..." + cancel: "Anullar" + comma: "," + comments: + hide: "Amagar los comentaris" + no_comments: "I a pas encara cap de comentari." + show: "Afichar totes los comentaris" + confirm_dialog: "Sètz segur ?" + confirm_unload: "Mercé de confirmar que volètz quitar aquesta pagina — las donadas sasidas seràn pas salvadas." + contacts: + add_contact: "Apondre aqueste contacte" + aspect_chat_is_enabled: "Los contactes d'aqueste aspècte pòdon discutir amb vos." + aspect_chat_is_not_enabled: "Los contactes d'aqueste aspècte pòdon pas discutir amb vos." + aspect_list_is_not_visible: "Los contactes d'aquesta facieta se pòdon pas veire entre eles." + aspect_list_is_visible: "Los contactes d'aqueste aspècte se pòdon veire entre eles." + error_add: "Impossible d'apondre <%= name %> a aquesta facieta :(" + error_remove: "Impossible de levar <%= name %> a aquesta facieta :(" + remove_contact: "Levar aqueste contacte" + search_no_results: "Cap de contact pas trobat" + conversation: + new: + no_contacts: "Vos cal apondre de contactes abans de poder aviar una conversacion." + create: "Crear" + delete: "Suprimir" + edit: "Editar" + failed_to_comment: "Impossible de comentar. Benlèu que l'autor vos ignòra ?" + failed_to_like: "Impossible de marcar que m'agrada !" + failed_to_post_message: "Impossible de partejar lo messatge !" + failed_to_remove: "L'entrada a pas pogut èsser suprimida" + failed_to_reshare: "Fracàs del partiment" + getting_started: + alright_ill_wait: "Bon, vau esperar." + hey: "Adiu, <%= name %>" + no_tags: "È, seguissètz pas cap de tag ! Contunar malgrat tot ?" + preparing_your_stream: "Preparacion de vòstre flux personalizat..." + header: + admin: "Administracion" + close: "Tampar" + contacts: "Contactes" + conversations: "Discussions" + help: "Ajuda" + home: "Acuèlh" + log_out: "Desconnexion" + mark_all_as_read: "Tot marcar coma legit" + moderator: "Moderator" + notifications: "Notificacions" + profile: "Perfil" + recent_notifications: "Administracion" + search: "Recercar" + settings: "Paramètres" + toggle_mobile: "Activar/desactivar la version mobila" + toggle_navigation: "Afichar/amagar lo menú" + view_all: "Afichar tot" + hide_post: "Amagar aquesta publicacion ?" + hide_post_failed: "Impossible d'amagar aqueste messatge" + ignore: "Ignorar" + ignore_failed: "Impossible d'ignorar aqueste utilizaire" + ignore_user: "Ignorar aqueste utilizaire ?" + my_activity: "Mon activitat" + my_aspects: "Mos aspèctes" + my_stream: "Flux" + no_results: "Pas cap de resultat" + notifications: + mark_read: "Marcar coma legit" + mark_unread: "Marcar coma pas legit" + people: + edit_my_profile: "Modificar mon perfil" + helper: + is_not_sharing: "<%= name %> parteja pas amb vos" + is_sharing: "<%= name %> parteja amb vos" + mention: "Mencion" + message: "Messatge" + not_found: "E degun es pas estat trobat..." + stop_ignoring: "Ignorar pas mai" + photo_uploader: + completed: "<%= file %> completat" + empty: "{file} es void, mercé de seleccionar d'autres fichièrs." + error: "Un problèma s'es produit pendent lo mandadís del fichièr <%= file %>" + invalid_ext: "{file} a una extension invalida, Solas {extensions} son permesas." + looking_good: "Impressionant, avètz un super look !" + size_error: "{file} es tròp gròs, la talha maximala es de {sizeLimit}." + poll: + answer_count: + one: "1 vòte" + other: "<%=count%> vòtes" + zero: "0 vòte" + close_result: "Amagar lo resultat" + count: + one: "<%=count%> vòte pel moment" + other: "<%=count%> vòtes pel moment" + zero: "Pas cap de vòte pel moment" + go_to_original_post: "Podètz participar a aqueste sondatge sul <%= original_post_link %>." + original_post: "messatge inicial" + result: "Resultat" + show_result: "Afichar lo resultat" + vote: "Vòte" + profile: + add_some: "apondre" + bio: "Biografia" + born: "Anniversari" + contacts: "Contactes" + edit: "Modificar" + gender: "Sèxe" + location: "Localizacion" + photos: "Fòtos" + posts: "Publicacions" + you_have_no_tags: "avètz pas de tag !" + publisher: + add_option: "Apondre una responsa" + markdown_editor: + preview: "Apercebut" + texts: + code: "còdi aicí" + heading: "Entèsta" + insert_image_description_text: "Entrar una descripcion per l'imatge aicí" + insert_image_help_text: "Inserir un ligam cap a un imatge aicí" + insert_image_title: "Entrar un intitulat aicí" + insert_link_description_text: "Entrar la descripcion del ligam aicí" + insert_link_help_text: "Inserir un ligam aicí" + italic: "Tèxte en italica" + list: "tèxte de lista aicí" + quote: "tèxte de citacion aicí" + strong: "Enfasi" + tooltips: + bold: "Gras" + cancel: "Anullar lo messatge" + code: "Inserir un còdi" + heading: "Títol" + insert_image: "Inserir un imatge" + insert_link: "Inserir un ligam" + insert_ordered_list: "Inserir una lista triada" + insert_unordered_list: "Inserir una lista de piuses" + italic: "Italica" + preview: "Apercebut del messatge" + quote: "Inserir una citacion" + write: "Modificar lo messatge" + write: "Escriure" + near_from: "Postat a : <%= location %>" + option: "Responsa" + public: "Public - vòstre messatge serà visible de totes e trobat pels motors de recèrca" + question: "Question" + remove_post: "Suprimir aqueste messatge ?" + report: + name: "Senhalar" + prompt: "Mercé de sasir un motiu :" + prompt_default: "Contengut ofensant" + status: + created: "Lo senhalament es estat creat amb succès." + exists: "Lo senhalament existís ja" + reshares: + duplicate: "Es tan plan coma aquò ? Avètz ja repartejat aqueste messatge !" + post: "Repartejar lo messatge de <%= name %> ?" + successful: "Lo messatge es estat repartejat !" + show_more: "N'afichar mai" + stream: + comment: "Comentari" + disable_post_notifications: "Desactivar las notificacions per aqueste messatge" + enable_post_notifications: "Activar las notificacions per aqueste messatge" + follow: "Seguir" + followed_tag: + add_a_tag: "Apondre una etiqueta" + follow: "Seguir" + title: "#tags seguits" + hide: "Amagar" + hide_nsfw_posts: "Amagar los messatges #nsfw" + like: "M'agrada" + likes: + one: "agrada a <%= count %> persona" + other: "agrada a <%= count %> personas" + zero: "agrada a <%= count %> persona" + limited: "Limitat" + more_comments: + one: "Afichar <%= count %> comentari suplementari" + other: "Afichar <%= count %> comentaris suplementaris" + zero: "Afichar <%= count %> comentari suplementari" + no_posts_yet: "I a pas cap de messatge a afichar pel moment." + original_post_deleted: "Lo messatge original es estat escafat per son autor." + permalink: "Permaligam" + public: "Public" + reactions: + one: "<%= count%> reaccion" + other: "<%= count%> reaccions" + zero: "<%= count%> reaccion" + reshare: "Repartejat" + reshares: + one: "<%= count %> repartiment" + other: "<%= count %> repartiments" + zero: "<%= count %> repartiment" + show_nsfw_post: "Mostrar lo messatge" + show_nsfw_posts: "Afichar tot" + tags: + follow: "Seguir #<%= tag %>" + follow_error: "Impossible de seguir <%= tag %> :(" + following: "Seguiment de #<%= tag %>" + stop_following: "Arrestar de seguir <%= tag %>" + stop_following_confirm: "Arrestar de seguir <%= tag %> ?" + stop_following_error: "Impossible d'arrestar de seguir <%= tag %> :(" + unfollow: "Seguir pas mai" + unlike: "M'agrada pas mai" + via: "via <%= provider %>" + tags: + wasnt_that_interesting: "D'acòrdi, supausi que #<%= tagName %> es pas la sola causa que vos interèssa..." + timeago: + day: "un jorn" + days: + one: "1 jorn" + other: "%d jorns" + hour: "environ una ora" + hours: + one: "environ 1 ora" + other: "environ %d oras" + inPast: "lèu lèu" + minute: "environ una minuta" + minutes: + one: "1 minuta" + other: "%d minutas" + month: "environ un mes" + months: + one: "1 mes" + other: "%d meses" + prefixAgo: "i a" + prefixFromNow: "d'aicí" + seconds: "mens d'una minuta" + suffixAgo: "" + suffixFromNow: "ara" + year: "environ un an" + years: + one: "1 an" + other: "%d ans" + unblock_failed: "Impossible de desblocar aqueste utilizaire" + viewer: + reshared: "Repartejat" \ No newline at end of file diff --git a/config/locales/javascript/javascript.sv.yml b/config/locales/javascript/javascript.sv.yml index 47aa80569..3424d104f 100644 --- a/config/locales/javascript/javascript.sv.yml +++ b/config/locales/javascript/javascript.sv.yml @@ -199,6 +199,14 @@ sv: publisher: add_option: "Lägg till alternativ" limited: "Begränsat: ditt inlägg visas endast för personer som du delar med" + markdown_editor: + tooltips: + bold: "Fet" + heading: "Rubrik" + insert_image: "Infoga bild" + insert_link: "Infoga länk" + italic: "Kursiv" + write: "Skriv" near_from: "Sänt från: <%= location %>" option: "Alternativ" public: "Publikt: ditt inlägg visas för alla och i söktjänster" @@ -266,16 +274,20 @@ sv: timeago: day: "en dag" days: + one: "1 dag" other: "%d dagar" hour: "ungefär en timme" hours: + one: "ungefär 1 timme" other: "ungefär %d timmar" inPast: "när som helst nu" minute: "ungefär en minut" minutes: + one: "1 minut" other: "%d minuter" month: "ungefär en månad" months: + one: "1 månad" other: "%d månader" prefixAgo: "för" prefixFromNow: "om" @@ -285,6 +297,7 @@ sv: wordSeparator: " " year: "ungefär ett år" years: + one: "1 år" other: "%d år" unblock_failed: "Misslyckades med att häva blockeringen" viewer: diff --git a/config/locales/javascript/javascript.te.yml b/config/locales/javascript/javascript.te.yml index 5c38cdfce..092cd9622 100644 --- a/config/locales/javascript/javascript.te.yml +++ b/config/locales/javascript/javascript.te.yml @@ -50,7 +50,7 @@ te: create: "సృష్టించు" delete: "తొలగించు" edit: "సవరించు" - failed_to_like: "ఇష్టపడుటలో విఫలమైంది!" + failed_to_like: "ఇష్టపడుటలో విఫలమైంది! బహుశా రచయిత మిమ్మల్ని విస్మరిస్తున్నారా?" failed_to_post_message: "సందేశాన్ని పోస్టుచేయుటలో విఫలమైంది!" failed_to_remove: "ప్రవేశాన్ని తొలగించుటలో విఫలమైంది!" failed_to_reshare: "మళ్ళీ పంచుకోవడంలో విఫలమైంది!" @@ -169,16 +169,20 @@ te: timeago: day: "ఒక రోజు" days: - other: "%d రోజుల" + one: "1 రోజు" + other: "%d రోజులు" hour: "దాదాపు గంట" hours: - other: "దాదాపు %d గంటల" + one: "దాదాపు 1 గంట" + other: "దాదాపు %d గంటలు" minute: "దాదాపు నిమిషం" minutes: - other: "%d నిమిషాల" + one: "1 నిమిషం" + other: "%d నిమిషాలు" month: "దాదాపు నెల" months: - other: "%d నెలల" + one: "1 నెల" + other: "%d నెలలు" prefixAgo: " " prefixFromNow: "" seconds: "కొన్ని క్షణాల" @@ -186,4 +190,5 @@ te: suffixFromNow: "ఇప్పటి నుండి" year: "దాదాపు సంవత్సరం" years: - other: "%d సంవత్సరాల" \ No newline at end of file + one: "1 సంవత్సరం" + other: "%d సంవత్సరాలు" \ No newline at end of file From 1c81c1e3ec62f0b586a4e45f8f9bcf770ac05003 Mon Sep 17 00:00:00 2001 From: Benjamin Neff Date: Sun, 16 Oct 2016 20:20:32 +0200 Subject: [PATCH 11/71] Refactor colors for better themeability --- .../stylesheets/bootstrap-variables.scss | 7 +++-- app/assets/stylesheets/color-variables.scss | 31 ++++++++++--------- .../_color_theme_override_origwhite.scss | 6 ---- .../color_themes/original_white/_style.scss | 1 - app/assets/stylesheets/contacts.scss | 12 +++---- app/assets/stylesheets/conversations.scss | 6 ++-- app/assets/stylesheets/forms.scss | 3 -- app/assets/stylesheets/header.scss | 2 +- app/assets/stylesheets/help.scss | 4 +-- app/assets/stylesheets/home.scss | 4 +-- app/assets/stylesheets/hovercard.scss | 2 +- app/assets/stylesheets/icons.scss | 2 +- app/assets/stylesheets/interactions.scss | 6 ++-- app/assets/stylesheets/invitations.scss | 8 ++--- app/assets/stylesheets/markdown-editor.scss | 6 ++-- app/assets/stylesheets/mobile/comments.scss | 2 +- app/assets/stylesheets/mobile/mobile.scss | 8 ++--- app/assets/stylesheets/mobile/tags.scss | 2 +- app/assets/stylesheets/notifications.scss | 4 +-- app/assets/stylesheets/profile.scss | 24 +++++++++----- app/assets/stylesheets/registration.scss | 2 +- app/assets/stylesheets/sidebar.scss | 4 +-- app/assets/stylesheets/stream_element.scss | 10 +++--- 23 files changed, 78 insertions(+), 78 deletions(-) diff --git a/app/assets/stylesheets/bootstrap-variables.scss b/app/assets/stylesheets/bootstrap-variables.scss index bca512cff..c8d8f634c 100644 --- a/app/assets/stylesheets/bootstrap-variables.scss +++ b/app/assets/stylesheets/bootstrap-variables.scss @@ -187,9 +187,9 @@ $btn-success-color: #333 !default; // $input-bg-disabled: $gray-lighter //** Text color for ``s -// $input-color: $gray +$input-color: $text-dark-grey !default; //** `` border color -// $input-border: #ccc +$input-border: $border-grey !default; // TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4 //** Default `.form-control` border radius @@ -202,6 +202,7 @@ $btn-success-color: #333 !default; //** Border color for inputs on focus // $input-border-focus: #66afe9 +$input-border-focus: $input-border !default; //** Placeholder text color // $input-color-placeholder: #999 @@ -668,7 +669,7 @@ $navbar-collapse-max-height: 480px; // //## //** Background color on `.list-group-item` -$list-group-bg: $white; +$list-group-bg: $white !default; //** `.list-group-item` border color $list-group-border: transparent; //** List group border radius diff --git a/app/assets/stylesheets/color-variables.scss b/app/assets/stylesheets/color-variables.scss index d3862a09d..40acec470 100644 --- a/app/assets/stylesheets/color-variables.scss +++ b/app/assets/stylesheets/color-variables.scss @@ -3,31 +3,32 @@ $black: #000; $text-grey: #999; $text-dark-grey: #666; -$text: #333; +$text-color-pale: $text-grey !default; +$text-color-active: $black !default; -$background-white: $white; -$background-grey: #eee; -$background-blue: #e7f2f7; +$background-grey: #eee !default; +$background-blue: #e7f2f7 !default; $grey: #2b2b2b; $medium-gray: #ccc; $light-grey: #ddd; -$border-grey: $light-grey; -$border-medium-grey: $medium-gray; -$border-dark-grey: $text-grey; -$border-medium-grey: #ccc; +$border-grey: $light-grey !default; +$border-medium-grey: $medium-gray !default; +$border-dark-grey: $text-grey !default; $link-grey: #777; -$link-disabled-grey: $text-grey; -$green: #8ede3d; +$icon-color: $black !default; + +$green: #8ede3d !default; $light-green: lighten($green, 20%); -$red: #a80000; -$blue: #3f8fba; +$red: #a80000 !default; +$blue: #3f8fba !default; -$main-background: #f0f0f0 !default; -$sidebars-background: $background-white !default; -$left-navbar-drawer-background: darken($sidebars-background, 6%); +$main-background: darken($white, 6%) !default; +$framed-background: $white !default; +$left-navbar-drawer-background: darken($white, 6%) !default; +$hovercard-background: $white !default; $card-shadow: 0 1px 2px 0 rgba(0, 0, 0, .16), 0 2px 10px 0 rgba(0, 0, 0, .12) !default; diff --git a/app/assets/stylesheets/color_themes/_color_theme_override_origwhite.scss b/app/assets/stylesheets/color_themes/_color_theme_override_origwhite.scss index c8e7fe62f..1d218888a 100644 --- a/app/assets/stylesheets/color_themes/_color_theme_override_origwhite.scss +++ b/app/assets/stylesheets/color_themes/_color_theme_override_origwhite.scss @@ -17,10 +17,4 @@ body { .left-navbar { border-right: 1px solid $border-grey; } - - .right-sidebar-fixed-background, - .right-sidebar-fixed-background, - .rightbar { - border-left: 1px solid $sidebars-background; - } } diff --git a/app/assets/stylesheets/color_themes/original_white/_style.scss b/app/assets/stylesheets/color_themes/original_white/_style.scss index 57228dfdf..448ce77f4 100644 --- a/app/assets/stylesheets/color_themes/original_white/_style.scss +++ b/app/assets/stylesheets/color_themes/original_white/_style.scss @@ -3,7 +3,6 @@ $background: #fff; // Variables $main-background: $background; -$sidebars-background: $background; $card-shadow: none; @import 'color_themes/color_theme_override_origwhite'; diff --git a/app/assets/stylesheets/contacts.scss b/app/assets/stylesheets/contacts.scss index a11cc8a93..4d49d39ad 100644 --- a/app/assets/stylesheets/contacts.scss +++ b/app/assets/stylesheets/contacts.scss @@ -38,13 +38,13 @@ margin-right: 25px; } #chat_privilege_toggle > .enabled { - color: #000; + color: $text-color-active; } .contacts-header-icon { font-size: 24.5px; line-height: 40px; - color: lighten($black,75%); - &:hover { color: $black; } + color: $text-color-pale; + &:hover { color: $text-color; } } #suggest_member.btn { margin-top: 8px; } } @@ -56,14 +56,14 @@ font-size: 20px; line-height: 50px; margin: 0 10px; - color: lighten($black,75%); - &:hover { color: $black; } + color: $text-color-pale; + &:hover { color: $text-color; } } &.in_aspect { border-left: 3px solid $brand-success; background-color: lighten($brand-success,35%); } - &:not(.in_aspect) { border-left: 3px solid $white; } + &:not(.in_aspect) { border-left: 3px solid $framed-background; } } .no_contacts { diff --git a/app/assets/stylesheets/conversations.scss b/app/assets/stylesheets/conversations.scss index efb89b669..dd443d646 100644 --- a/app/assets/stylesheets/conversations.scss +++ b/app/assets/stylesheets/conversations.scss @@ -17,7 +17,7 @@ } .stream-element { - background-color: $white; + background-color: $framed-background; padding: 10px; .avatar { @@ -30,7 +30,7 @@ .stream-element.message, .stream-element.new-message { - border: 1px solid $light-grey; + border: 1px solid $border-grey; box-shadow: $card-shadow; margin-bottom: 20px; @@ -83,7 +83,7 @@ } } - &.unread { background-color: darken($background-white, 5%); } + &.unread { background-color: $background-grey; } &.selected { background-color: $blue; } .last_author, .last_message { diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index 98a02a623..2f0717bba 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -13,9 +13,7 @@ textarea { &:active:focus, &:invalid:focus, &:invalid:required:focus { - border-color: $border-grey; box-shadow: none; - color: $text-dark-grey; } } // scss-lint:enable QualifyingElement @@ -29,7 +27,6 @@ textarea { margin: 20px auto; fieldset { - background-color: $white; margin-bottom: 1em; position: relative; // To correctly place the entypo icon diff --git a/app/assets/stylesheets/header.scss b/app/assets/stylesheets/header.scss index 4d7aa516b..668cef21c 100644 --- a/app/assets/stylesheets/header.scss +++ b/app/assets/stylesheets/header.scss @@ -106,7 +106,7 @@ } .view_all { background-color: $link-color; - border-top: 3px solid $white; + border-top: 3px solid $dropdown-bg; text-align: center; a { color: $white; diff --git a/app/assets/stylesheets/help.scss b/app/assets/stylesheets/help.scss index d5a864770..c19e09439 100644 --- a/app/assets/stylesheets/help.scss +++ b/app/assets/stylesheets/help.scss @@ -98,9 +98,9 @@ ul#help_nav { line-height: 70px; [class^="entypo-"], [class*="entypo-"] { - color: #bfbfbf; + color: $text-color-pale; - &.entypo-chat{ color: #000000; } + &.entypo-chat { color: $text-color-active; } } } } diff --git a/app/assets/stylesheets/home.scss b/app/assets/stylesheets/home.scss index b661e4b4f..1dd6555ad 100644 --- a/app/assets/stylesheets/home.scss +++ b/app/assets/stylesheets/home.scss @@ -33,8 +33,8 @@ } .landing-info-card { - background-color: $white; - border: 1px solid $light-grey; + background-color: $framed-background; + border: 1px solid $border-grey; box-shadow: $card-shadow; margin-bottom: 25px; margin-top: 25px; diff --git a/app/assets/stylesheets/hovercard.scss b/app/assets/stylesheets/hovercard.scss index 1cf9ae620..ffe8e3cb8 100644 --- a/app/assets/stylesheets/hovercard.scss +++ b/app/assets/stylesheets/hovercard.scss @@ -7,7 +7,7 @@ min-width: 250px; max-width: 400px; - background-color: $background-white; + background-color: $hovercard-background; border: 1px solid $border-dark-grey; font-size: small; diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index b4496bbd3..a6cc1f917 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -1,6 +1,6 @@ [class^="entypo-"], [class*="entypo-"] { font-style: normal; - color: black; + color: $icon-color; &.red { color: #A40802; } &.white { color: white; } diff --git a/app/assets/stylesheets/interactions.scss b/app/assets/stylesheets/interactions.scss index e2f95d677..211f27acd 100644 --- a/app/assets/stylesheets/interactions.scss +++ b/app/assets/stylesheets/interactions.scss @@ -4,7 +4,7 @@ [class^="entypo-"], [class*="entypo-"] { - color: $text-grey; + color: $text-color-pale; font-size: $font-size-base; line-height: $line-height-base; vertical-align: middle; @@ -12,12 +12,12 @@ [class^="entypo-"]:hover, [class*="entypo-"]:hover { - color: $text; + color: $text-color; } &.hide_conversation i { font-size: $line-height-computed * 1.5; } &.delete_conversation i { font-size: $font-size-base * 1.5; } - &.destroy_participation i { color: $black; } + &.destroy_participation i { color: $text-color-active; } &.destroy_participation i:hover { color: $text-dark-grey; } } } diff --git a/app/assets/stylesheets/invitations.scss b/app/assets/stylesheets/invitations.scss index a78bed09f..f7f7451d2 100644 --- a/app/assets/stylesheets/invitations.scss +++ b/app/assets/stylesheets/invitations.scss @@ -1,5 +1,5 @@ #invite_code { - background-color: $white; + background-color: $framed-background; cursor: text; display: block; margin-top: 5px; @@ -7,13 +7,13 @@ #invitationsModal { .modal-header, .modal-body { - color: $text; + color: $text-color; font-size: $font-size-base; text-align: initial; } #paste_link { font-weight: 700; } #invite_code { margin-top: 10px; } - #codes_left { color: $text-grey; } + #codes_left { color: $text-color-pale; } .controls { margin-left: 140px; } #email_invitation { padding-top: 10px; @@ -21,7 +21,7 @@ border-top: 1px dashed $border-grey; label { font-weight: 700; } #already_sent { - color: $text-grey; + color: $text-color-pale; font-size: 12px; } } diff --git a/app/assets/stylesheets/markdown-editor.scss b/app/assets/stylesheets/markdown-editor.scss index cb731118b..e6a679258 100644 --- a/app/assets/stylesheets/markdown-editor.scss +++ b/app/assets/stylesheets/markdown-editor.scss @@ -1,6 +1,6 @@ .md-footer, .md-header { - background: $sidebars-background; + background: $white; border: 0; display: block; height: 42px; @@ -10,7 +10,7 @@ [class^="entypo-"], [class*="entypo-"], .glyphicon { - color: $black; + color: $icon-color; } } @@ -72,7 +72,7 @@ width: 18px; } - &:hover .entypo-cross { color: $text; } + &:hover .entypo-cross { color: $text-color; } } diff --git a/app/assets/stylesheets/mobile/comments.scss b/app/assets/stylesheets/mobile/comments.scss index 7f5382c37..e77bf196a 100644 --- a/app/assets/stylesheets/mobile/comments.scss +++ b/app/assets/stylesheets/mobile/comments.scss @@ -33,7 +33,7 @@ &.active:not(.bottom_collapse), &.active:not(.bottom_collapse) > [class^="entypo"] { - color: $text; + color: $text-color; } } diff --git a/app/assets/stylesheets/mobile/mobile.scss b/app/assets/stylesheets/mobile/mobile.scss index 7996bcee9..1b16f4d09 100644 --- a/app/assets/stylesheets/mobile/mobile.scss +++ b/app/assets/stylesheets/mobile/mobile.scss @@ -150,7 +150,7 @@ footer { border-radius: 5px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); - background-color: #fff; + background-color: $framed-background; margin-bottom: 10px; border: 1px solid #bbb; @@ -176,7 +176,7 @@ footer { border-radius: 3px 3px 0 0; border: { - bottom: 1px solid #ccc; + bottom: 1px solid $border-medium-grey; } img.big-stream-photo { @@ -322,7 +322,7 @@ footer { } .header-full-width { - background-color: #fff; + background-color: $framed-background; border-bottom: 1px solid #aaa; margin: -10px; // Counter the #main padding margin-bottom: 10px; @@ -462,7 +462,7 @@ select { font-weight: bold; color: $text-grey; background: { - color: #fff; + color: $framed-background; } } } diff --git a/app/assets/stylesheets/mobile/tags.scss b/app/assets/stylesheets/mobile/tags.scss index ba5f0ac67..665e41d98 100644 --- a/app/assets/stylesheets/mobile/tags.scss +++ b/app/assets/stylesheets/mobile/tags.scss @@ -3,7 +3,7 @@ ul.followed_tags { margin: 0px; > li { - background-color: $white; + background-color: $framed-background; border: 1px solid $border-grey; border-radius: 5px; box-shadow: 0 1px 2px rgba($border-dark-grey, 0.5); diff --git a/app/assets/stylesheets/notifications.scss b/app/assets/stylesheets/notifications.scss index 16f12513c..27df15086 100644 --- a/app/assets/stylesheets/notifications.scss +++ b/app/assets/stylesheets/notifications.scss @@ -73,7 +73,7 @@ background-color: $background-grey; .unread-toggle { opacity: 1 !important; - .entypo-eye { color: $black; } + .entypo-eye { color: $text-color-active; } } } @@ -90,7 +90,7 @@ padding: 9px 5px; .entypo-eye { cursor: pointer; - color: lighten($black,75%); + color: $text-color-pale; font-size: 17px; line-height: 17px; } diff --git a/app/assets/stylesheets/profile.scss b/app/assets/stylesheets/profile.scss index 727c99fee..cec99ef33 100644 --- a/app/assets/stylesheets/profile.scss +++ b/app/assets/stylesheets/profile.scss @@ -1,7 +1,7 @@ #profile_container { .profile_header { margin-bottom: 15px; - background-color: $white; + background-color: $framed-background; padding-left: 10px; padding-right: 10px; padding-top: 20px; @@ -22,7 +22,7 @@ font-weight: 700; } #diaspora_handle { - color: $text-grey; + color: $text-color-pale; font-size: 20px; } #sharing_message { @@ -64,8 +64,8 @@ .profile-header-icon { font-size: 24.5px; line-height: 30px; - color: lighten($black,75%); - &:hover { color: $black; } + color: $text-color-pale; + &:hover { color: $text-color; } } #mention_button { font-weight: 700; } } @@ -80,8 +80,12 @@ &.active { border-bottom: 3px solid $brand-primary; a { - color: $black; - [class^="entypo-"], [class*="entypo-"] { color: $black; } + color: $text-color-active; + + [class^="entypo-"], + [class*="entypo-"] { + color: $text-color-active; + } } } a { @@ -94,9 +98,13 @@ margin-right: 2px; } &:hover { - color: $black; - [class^="entypo-"], [class*="entypo-"] { color: $black; } + color: $text-color-active; text-decoration: none; + + [class^="entypo-"], + [class*="entypo-"] { + color: $text-color-active; + } } } } diff --git a/app/assets/stylesheets/registration.scss b/app/assets/stylesheets/registration.scss index 53c65674d..123161d6a 100644 --- a/app/assets/stylesheets/registration.scss +++ b/app/assets/stylesheets/registration.scss @@ -35,7 +35,7 @@ } .captcha-input { - border-bottom: 1px solid $border-grey; + border-bottom: 1px solid $input-border; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; box-sizing: border-box; diff --git a/app/assets/stylesheets/sidebar.scss b/app/assets/stylesheets/sidebar.scss index 1df833b42..a09f5d84b 100644 --- a/app/assets/stylesheets/sidebar.scss +++ b/app/assets/stylesheets/sidebar.scss @@ -1,7 +1,7 @@ .sidebar, .framed-content { - background-color: $white; - border: 1px solid $light-grey; + background-color: $framed-background; + border: 1px solid $border-grey; border-top: 0; box-shadow: $card-shadow; diff --git a/app/assets/stylesheets/stream_element.scss b/app/assets/stylesheets/stream_element.scss index e9f466676..317c8db7e 100644 --- a/app/assets/stylesheets/stream_element.scss +++ b/app/assets/stylesheets/stream_element.scss @@ -15,7 +15,7 @@ position: relative; .control-icons { - background: $white; + background: $framed-background; border-radius: 4px; padding-left: 4px; position: absolute; @@ -26,7 +26,7 @@ } .thumbnail { - background: $white; + background: $framed-background; border-radius: 0; box-shadow: $card-shadow; height: 240px; @@ -40,7 +40,7 @@ &:hover, &:focus, &:active { - border-color: $light-grey; + border-color: $border-grey; text-decoration: none; } @@ -62,7 +62,7 @@ #main_stream .stream-element { margin-bottom: 20px; - border: 1px solid $light-grey; + border: 1px solid $border-grey; box-shadow: $card-shadow; &.highlighted { @@ -72,7 +72,7 @@ } .stream-element { - background-color: $white; + background-color: $framed-background; padding: 10px; & > .media { From 08282cea0143451758f8443ba6667c050d34dc72 Mon Sep 17 00:00:00 2001 From: Benjamin Neff Date: Mon, 17 Oct 2016 03:38:13 +0200 Subject: [PATCH 12/71] Add dark theme Also renamed "Original Dark" to "Original Gray", because it isn't "dark". closes #7152 --- Changelog.md | 1 + .../_color_theme_override_dark.scss | 170 ++++++++++++++++++ .../stylesheets/color_themes/dark/_style.scss | 153 ++++++++++++++++ .../color_themes/dark/desktop.scss | 3 + .../stylesheets/color_themes/dark/mobile.scss | 63 +++++++ config/color_themes.yml | 3 +- config/initializers/color_themes.rb | 4 +- 7 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 app/assets/stylesheets/color_themes/_color_theme_override_dark.scss create mode 100644 app/assets/stylesheets/color_themes/dark/_style.scss create mode 100644 app/assets/stylesheets/color_themes/dark/desktop.scss create mode 100644 app/assets/stylesheets/color_themes/dark/mobile.scss diff --git a/Changelog.md b/Changelog.md index bfe47c7e8..0e8a75305 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,6 +8,7 @@ ## Features * Show spinner when loading comments in the stream [#7170](https://github.com/diaspora/diaspora/pull/7170) +* Add a dark color theme [#7152](https://github.com/diaspora/diaspora/pull/7152) # 0.6.1.0 diff --git a/app/assets/stylesheets/color_themes/_color_theme_override_dark.scss b/app/assets/stylesheets/color_themes/_color_theme_override_dark.scss new file mode 100644 index 000000000..1719460f9 --- /dev/null +++ b/app/assets/stylesheets/color_themes/_color_theme_override_dark.scss @@ -0,0 +1,170 @@ +// Only overriding existing selectors here, so disable some lint rules +// scss-lint:disable IdSelector, SelectorFormat, NestingDepth, SelectorDepth, QualifyingElement +body { + .navbar.navbar-fixed-top #user_menu .dropdown-menu > li > a { + color: $text-color; + &:hover { color: $white; } + } + + .publisher { + .mentions-input-box { background-color: $gray; } + form { + #publisher_textarea_wrapper { background-color: $gray; } + .btn.btn-link.question_mark:hover .entypo-cog { color: $gray-light; } + } + + .write-preview-tabs > li.active * { color: $text-color; } + .md-preview { background-color: $gray; } + .md-cancel:hover .entypo-cross { color: $gray-light; } + .publisher-buttonbar .btn.btn-link:hover i { color: $gray-light; } + } + + .aspect_dropdown li a .text { color: $dropdown-link-color; } + + .info .tag { background-color: $gray-light; } + + .poll_form .progress { + background-color: $gray-dark; + .bar { background-color: $gray-light; } + } + + .stream-element .collapsible { + .markdown-content hr { border-top: 1px solid $hr-border; } + .expander { + @include linear-gradient(transparent, $gray-light, 0%, 95%); + border-bottom: 2px solid $gray-light; + color: $text-color; + text-shadow: 0 0 7px $black; + } + } + + code, + pre { + background-color: $gray-dark; + border: 1px solid $border-medium-grey; + color: $text-color; + } + + pre code { border: 0; } + + @import 'highlightjs/darcula'; + + #single-post-content .head { + #post-info .author { color: lighten($gray-lighter, 27%); } + #single-post-actions i.entypo-heart.red:hover { color: $red; } + } + + .opengraph a { color: lighten($gray-lighter, 27%); } + + .tag:hover { background-color: desaturate(darken($blue, 35%), 20%); } + + #profile_container .profile_header { + #author_info #sharing_message.entypo-check { color: lighten($green, 10%); } + } + + #invitationsModal #email_invitation { border-top: 1px dashed $gray-light; } + + #contacts_container #people_stream.contacts .stream-element.in_aspect { + background-color: $state-success-bg; + border-left: 3px solid darken($state-success-bg, 10%); + } + + .left-navbar #tags_list { + .as-list { + color: $text-color; + + em { + background-color: lighten($background-blue, 10%); + color: $text-color; + } + } + + .as-result-item.active { color: $text-color; } + } + + #faq .question { + background-color: $gray-dark; + a.toggle { color: $gray-lighter; } + &.collapsed { border: 2px solid $gray-dark; } + &.opened { + border: 2px solid darken($green, 10%); + h4 { background-color: darken($green, 10%); } + } + + .answer { background-color: $gray; } + } + + #welcome-to-diaspora { background: $orange; } + + .block-form fieldset .form-control:focus { border-color: $input-border; } + + &.page-registrations.action-new, + &.page-registrations.action-create { + .ball { filter: invert(100%); } + } + + .spinner { border-color: $gray-light transparent $gray-light $gray-light; } + + // AutoSuggest CSS + ul.as-selections { + background-color: $framed-background; + + li.as-selection-item, + li.as-selection-item.blur { + background-color: $gray-dark; + border: 1px solid $gray-darker; + box-shadow: 0 1px 1px $gray-darker; + color: $text-color; + text-shadow: 0 1px 1px $gray-darker; + } + + li.as-selection-item a.as-close, + li.as-selection-item.blur a.as-close { + color: $text-color; + text-shadow: 0 1px 1px $gray-darker; + } + + li:hover.as-selection-item { + background-color: $light-blue; + border-color: $brand-primary; + color: $white; + a.as-close { color: $gray-light; } + } + + li.as-selection-item.selected { border-color: $brand-primary; } + li.as-selection-item a:hover.as-close { color: $white; } + li.as-selection-item a:active.as-close { color: $gray-lighter; } + } + + ul.as-list { + background-color: $gray-dark; + box-shadow: 0 2px 12px $gray-light; + color: $text-color; + } + + li.as-result-item, + li.as-message { + border: 1px solid $gray-dark; + } + + li.as-result-item.active { + background-color: $brand-primary; + border-color: $brand-primary; + text-shadow: none; + + em { background: darken($brand-primary, 10%); } + } + // End AutoSuggest CSS + + // Bootstrap Switch CSS + .bootstrap-switch { + border-color: $border-grey; + .bootstrap-switch-label { background: $framed-background; } + .bootstrap-switch-handle-on.bootstrap-switch-default, + .bootstrap-switch-handle-off.bootstrap-switch-default { + background: $gray-dark; + color: $text-color; + } + } + // End Bootstrap Switch CSS +} diff --git a/app/assets/stylesheets/color_themes/dark/_style.scss b/app/assets/stylesheets/color_themes/dark/_style.scss new file mode 100644 index 000000000..1078d86bc --- /dev/null +++ b/app/assets/stylesheets/color_themes/dark/_style.scss @@ -0,0 +1,153 @@ +// Main color(s) +$white: #fff; +$black: #000; + +$gray-base: $black; +$gray-darker: lighten($gray-base, 6%); +$gray-dark: lighten($gray-base, 9.5%); +$gray: lighten($gray-base, 13.5%); +$gray-light: lighten($gray-base, 28%); +$gray-lighter: lighten($gray-base, 58%); + +$green: #346535; +$red: #622; +$blue: #4183c4; +$yellow: #645a1b; +$orange: #664100; + +$light-blue: lighten($blue, 5%); + +$brand-primary: darken($blue, 5%); +$brand-success: $green; +$brand-info: darken(adjust-hue($brand-primary, -30), 15%); +$brand-danger: lighten($red, 10%); + +// Bootstrap Variables +//== Scaffolding +$body-bg: $gray; +$text-color: lighten($gray-lighter, 17%); +$link-color: $blue; + +//== Tables +$table-bg-accent: $gray-dark; +$table-border-color: $gray-light; + +//== Buttons +$btn-default-color: $gray-lighter; +$btn-default-bg: $gray-light; +$btn-default-border: $gray-darker; + +$btn-success-color: $white; + +//== Forms +$input-bg: $gray-dark; + +$input-color: $text-color; +$input-border: $gray-light; +$input-border-focus: $brand-primary; + +$input-color-placeholder: lighten($gray-light, 7%); + +$legend-color: $text-color; +$legend-border-color: $gray-light; + +//== Dropdowns +$dropdown-bg: lighten($gray-base, 15%); +$dropdown-divider-bg: $gray-darker; +$dropdown-link-color: $text-color; +$dropdown-link-hover-color: $dropdown-link-color; + +//== Navbar +$navbar-inverse-bg: $gray-darker; +$navbar-inverse-link-hover-color: $text-color; +$navbar-inverse-brand-hover-color: $navbar-inverse-link-hover-color; + +//== Tabs +$nav-tabs-active-link-hover-bg: $gray; +$nav-tabs-active-link-hover-border-color: $gray-darker; + +//== Navs +$nav-link-hover-bg: $gray-darker; + +//== Pagination +$pagination-color: $light-blue; +$pagination-bg: $gray-light; +$pagination-border: $gray-darker; + +$pagination-hover-color: $gray-dark; +$pagination-hover-bg: $light-blue; +$pagination-hover-border: $pagination-border; + +$pagination-active-border: $pagination-border; + +$pagination-disabled-color: $gray-dark; +$pagination-disabled-bg: $gray-light; +$pagination-disabled-border: $pagination-border; + +//== Form states and alerts +$state-success-text: lighten($green, 30%); +$state-success-bg: darken($green, 10%); +$state-success-border: darken($state-success-bg, 20%); + +$state-info-text: lighten($blue, 20%); +$state-info-bg: darken($blue, 20%); +$state-info-border: darken($state-info-bg, 20%); + +$state-warning-text: lighten($yellow, 30%); +$state-warning-bg: $yellow; +$state-warning-border: darken($state-warning-bg, 20%); + +$state-danger-text: lighten($red, 40%); +$state-danger-bg: $red; +$state-danger-border: darken($state-danger-bg, 20%); + + +//== Popovers +$popover-bg: lighten($gray, 5%); +$popover-border-color: $gray-darker; + +//== Modals +$modal-content-bg: $gray; +$modal-header-border-color: $gray-light; + +//== List group +$list-group-bg: $gray; +$list-group-link-color: $text-color; + +//== Panels +$panel-bg: $gray; +$panel-default-text: $text-color; +$panel-default-border: $gray-darker; +$panel-default-heading-bg: $gray-dark; + +//== Thumbnails +$thumbnail-border: $gray-darker; + +//== Wells +$well-bg: $gray-dark; + +//== Close +$close-color: $gray-lighter; + +//== Type +$hr-border: $gray-light; + +// Variables +$text-color-pale: $gray-light; +$text-color-active: lighten($gray-lighter, 27%); + +$background-grey: $gray-dark; +$background-blue: desaturate(darken($blue, 25%), 15%); + +$border-grey: $gray-darker; +$border-medium-grey: $gray-light; +$border-dark-grey: darken($border-grey, 4.5%); + +$icon-color: $text-color; + +$main-background: $gray-dark; +$framed-background: $gray; +$left-navbar-drawer-background: $main-background; +$hovercard-background: $gray; + +@import 'color_themes/color_theme_override_dark'; diff --git a/app/assets/stylesheets/color_themes/dark/desktop.scss b/app/assets/stylesheets/color_themes/dark/desktop.scss new file mode 100644 index 000000000..92ef0cbea --- /dev/null +++ b/app/assets/stylesheets/color_themes/dark/desktop.scss @@ -0,0 +1,3 @@ +@import 'mixins'; +@import 'color_themes/dark/style'; +@import 'application'; diff --git a/app/assets/stylesheets/color_themes/dark/mobile.scss b/app/assets/stylesheets/color_themes/dark/mobile.scss new file mode 100644 index 000000000..39846a50b --- /dev/null +++ b/app/assets/stylesheets/color_themes/dark/mobile.scss @@ -0,0 +1,63 @@ +@import 'mixins'; +@import 'color_themes/dark/style'; + +// Only overriding existing selectors here, so disable some lint rules +// scss-lint:disable SelectorFormat, NestingDepth, SelectorDepth +body { + .settings-container, + .stream-element, + .login-form { + border: 1px solid $border-grey; + } + + .stream-element, + .comments { + .from a { color: $text-color; } + .info { color: lighten($gray-light, 12%); } + .nsfw-shield { background-color: $gray-light; } + + .bottom-bar { + background: lighten($framed-background, 4.5%); + .post-action .disabled { color: $text-color-pale; } + .post-stats .count { background-color: lighten($framed-background, 4.5%); } + } + + .reshare { + border-bottom: 1px solid $border-medium-grey; + .reshare_via span { color: $border-medium-grey; } + } + } + + .more-link, + .no-more-posts { + background: { color: $btn-default-bg; } + border: 1px solid $gray; + + h1, + h2 { + color: $text-color; + text-shadow: 0 2px 0 $gray; + } + } + + .stream-element.unread { background-color: $gray; } + .stream-element.read { background-color: $gray-darker; } + + .header-full-width { border-bottom: 1px solid $border-grey; } + + .user_aspects { + &, + &:focus, + &:active { + border-color: $gray-light; + } + + &.has_connection { + background-color: $green; + color: $white; + } + } +} +// scss-lint:enable IdSelector, SelectorFormat, NestingDepth, SelectorDepth + +@import 'mobile/mobile'; diff --git a/config/color_themes.yml b/config/color_themes.yml index 6aaa75dfc..896658841 100644 --- a/config/color_themes.yml +++ b/config/color_themes.yml @@ -1,6 +1,7 @@ available: - original: "Original Dark" + original: "Original Gray" original_white: "Original White Background" dark_green: "Dark Green" magenta: "Magenta" egyptian_blue: "Egyptian Blue" + dark: "Dark" diff --git a/config/initializers/color_themes.rb b/config/initializers/color_themes.rb index 9c12af8a6..6f76ee51c 100644 --- a/config/initializers/color_themes.rb +++ b/config/initializers/color_themes.rb @@ -10,10 +10,10 @@ if color_themes_file.exist? if color_themes["available"].length > 0 color_themes["available"] else - {"original" => "Original Dark"} + {"original" => "Original Gray"} end else - AVAILABLE_COLOR_THEMES = {"original" => "Original Dark"} + AVAILABLE_COLOR_THEMES = {"original" => "Original Gray"}.freeze end # Get all codes from available themes into a separate variable, so that they can be called easier. AVAILABLE_COLOR_THEME_CODES = AVAILABLE_COLOR_THEMES.keys From 57c033053575e7c19db7912625c907068f4af19f Mon Sep 17 00:00:00 2001 From: Benjamin Neff Date: Tue, 25 Oct 2016 23:40:13 +0200 Subject: [PATCH 13/71] Schedule a connection-check when receiving a message from an offline pod closes #7158 --- app/models/pod.rb | 9 ++++ app/workers/recheck_scheduled_pods.rb | 9 ++++ config/initializers/diaspora_federation.rb | 4 +- config/schedule.yml | 4 ++ ...161024231443_add_scheduled_check_to_pod.rb | 5 ++ db/schema.rb | 23 +++++----- spec/federation_callbacks_spec.rb | 22 +++++++-- spec/models/pod_spec.rb | 46 +++++++++++++++++++ spec/workers/recheck_offline_pods_spec.rb | 12 +++++ 9 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 app/workers/recheck_scheduled_pods.rb create mode 100644 db/migrate/20161024231443_add_scheduled_check_to_pod.rb create mode 100644 spec/workers/recheck_offline_pods_spec.rb diff --git a/app/models/pod.rb b/app/models/pod.rb index a961353d6..dde1e28f1 100644 --- a/app/models/pod.rb +++ b/app/models/pod.rb @@ -61,6 +61,10 @@ class Pod < ActiveRecord::Base def check_all! Pod.find_in_batches(batch_size: 20) {|batch| batch.each(&:test_connection!) } end + + def check_scheduled! + Pod.where(scheduled_check: true).find_each(&:test_connection!) + end end def offline? @@ -76,6 +80,10 @@ class Pod < ActiveRecord::Base "#{id}:#{host}" end + def schedule_check_if_needed + update_column(:scheduled_check, true) if offline? && !scheduled_check + end + def test_connection! result = ConnectionTester.check uri.to_s logger.debug "tested pod: '#{uri}' - #{result.inspect}" @@ -108,6 +116,7 @@ class Pod < ActiveRecord::Base attributes_from_result(result) touch(:checked_at) + self.scheduled_check = false save end diff --git a/app/workers/recheck_scheduled_pods.rb b/app/workers/recheck_scheduled_pods.rb new file mode 100644 index 000000000..8ca2be921 --- /dev/null +++ b/app/workers/recheck_scheduled_pods.rb @@ -0,0 +1,9 @@ +module Workers + class RecheckScheduledPods < Base + sidekiq_options queue: :low + + def perform + Pod.check_scheduled! + end + end +end diff --git a/config/initializers/diaspora_federation.rb b/config/initializers/diaspora_federation.rb index 1b549fb17..49ff3da6c 100644 --- a/config/initializers/diaspora_federation.rb +++ b/config/initializers/diaspora_federation.rb @@ -93,7 +93,9 @@ DiasporaFederation.configure do |config| end end - on :receive_entity do |entity, _sender, recipient_id| + on :receive_entity do |entity, sender, recipient_id| + Person.by_account_identifier(sender).pod.try(:schedule_check_if_needed) + case entity when DiasporaFederation::Entities::AccountDeletion Diaspora::Federation::Receive.account_deletion(entity) diff --git a/config/schedule.yml b/config/schedule.yml index 5ba7e5cd6..0d179a75b 100644 --- a/config/schedule.yml +++ b/config/schedule.yml @@ -9,3 +9,7 @@ queue_users_for_removal: recurring_pod_check: cron: "0 0 * * *" class: "Workers::RecurringPodCheck" + +recheck_scheduled_pods: + cron: "*/30 * * * *" + class: "Workers::RecheckScheduledPods" diff --git a/db/migrate/20161024231443_add_scheduled_check_to_pod.rb b/db/migrate/20161024231443_add_scheduled_check_to_pod.rb new file mode 100644 index 000000000..6d18ba228 --- /dev/null +++ b/db/migrate/20161024231443_add_scheduled_check_to_pod.rb @@ -0,0 +1,5 @@ +class AddScheduledCheckToPod < ActiveRecord::Migration + def change + add_column :pods, :scheduled_check, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index c8f63ceb5..402f53db2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161015174300) do +ActiveRecord::Schema.define(version: 20161024231443) do create_table "account_deletions", force: :cascade do |t| t.string "diaspora_handle", limit: 255 @@ -358,18 +358,19 @@ ActiveRecord::Schema.define(version: 20161015174300) do add_index "photos", ["status_message_guid"], name: "index_photos_on_status_message_guid", length: {"status_message_guid"=>191}, using: :btree create_table "pods", force: :cascade do |t| - t.string "host", limit: 255, null: false + t.string "host", limit: 255, null: false t.boolean "ssl" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "status", limit: 4, default: 0 - t.datetime "checked_at", default: '1970-01-01 00:00:00' + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "status", limit: 4, default: 0 + t.datetime "checked_at", default: '1970-01-01 00:00:00' t.datetime "offline_since" - t.integer "response_time", limit: 4, default: -1 - t.string "software", limit: 255 - t.string "error", limit: 255 - t.integer "port", limit: 4 - t.boolean "blocked", default: false + t.integer "response_time", limit: 4, default: -1 + t.string "software", limit: 255 + t.string "error", limit: 255 + t.integer "port", limit: 4 + t.boolean "blocked", default: false + t.boolean "scheduled_check", default: false, null: false end add_index "pods", ["checked_at"], name: "index_pods_on_checked_at", using: :btree diff --git a/spec/federation_callbacks_spec.rb b/spec/federation_callbacks_spec.rb index 349492713..73193f824 100644 --- a/spec/federation_callbacks_spec.rb +++ b/spec/federation_callbacks_spec.rb @@ -338,7 +338,7 @@ describe "diaspora federation callbacks" do describe ":receive_entity" do it "receives an AccountDeletion" do - account_deletion = FactoryGirl.build(:account_deletion_entity) + account_deletion = FactoryGirl.build(:account_deletion_entity, author: remote_person.diaspora_handle) expect(Diaspora::Federation::Receive).to receive(:account_deletion).with(account_deletion) expect(Workers::ReceiveLocal).not_to receive(:perform_async) @@ -347,7 +347,7 @@ describe "diaspora federation callbacks" do end it "receives a Retraction" do - retraction = FactoryGirl.build(:retraction_entity) + retraction = FactoryGirl.build(:retraction_entity, author: remote_person.diaspora_handle) expect(Diaspora::Federation::Receive).to receive(:retraction).with(retraction, 42) expect(Workers::ReceiveLocal).not_to receive(:perform_async) @@ -356,7 +356,7 @@ describe "diaspora federation callbacks" do end it "receives a entity" do - received = FactoryGirl.build(:status_message_entity) + received = FactoryGirl.build(:status_message_entity, author: remote_person.diaspora_handle) persisted = FactoryGirl.create(:status_message) expect(Diaspora::Federation::Receive).to receive(:perform).with(received).and_return(persisted) @@ -365,8 +365,20 @@ describe "diaspora federation callbacks" do DiasporaFederation.callbacks.trigger(:receive_entity, received, received.author, nil) end + it "calls schedule_check_if_needed on the senders pod" do + received = FactoryGirl.build(:status_message_entity, author: remote_person.diaspora_handle) + persisted = FactoryGirl.create(:status_message) + + expect(Person).to receive(:by_account_identifier).with(received.author).and_return(remote_person) + expect(remote_person.pod).to receive(:schedule_check_if_needed) + expect(Diaspora::Federation::Receive).to receive(:perform).with(received).and_return(persisted) + expect(Workers::ReceiveLocal).to receive(:perform_async).with(persisted.class.to_s, persisted.id, []) + + DiasporaFederation.callbacks.trigger(:receive_entity, received, received.author, nil) + end + it "receives a entity for a recipient" do - received = FactoryGirl.build(:status_message_entity) + received = FactoryGirl.build(:status_message_entity, author: remote_person.diaspora_handle) persisted = FactoryGirl.create(:status_message) expect(Diaspora::Federation::Receive).to receive(:perform).with(received).and_return(persisted) @@ -376,7 +388,7 @@ describe "diaspora federation callbacks" do end it "does not trigger a ReceiveLocal job if Receive.perform returned nil" do - received = FactoryGirl.build(:status_message_entity) + received = FactoryGirl.build(:status_message_entity, author: remote_person.diaspora_handle) expect(Diaspora::Federation::Receive).to receive(:perform).with(received).and_return(nil) expect(Workers::ReceiveLocal).not_to receive(:perform_async) diff --git a/spec/models/pod_spec.rb b/spec/models/pod_spec.rb index beb283414..771377d44 100644 --- a/spec/models/pod_spec.rb +++ b/spec/models/pod_spec.rb @@ -82,6 +82,16 @@ describe Pod, type: :model do end end + describe ".check_scheduled!" do + it "calls #test_connection! on all scheduled pods" do + (0..4).map { FactoryGirl.create(:pod) } + FactoryGirl.create(:pod, scheduled_check: true) + + expect_any_instance_of(Pod).to receive(:test_connection!) + Pod.check_scheduled! + end + end + describe "#active?" do it "returns true for an unchecked pod" do pod = FactoryGirl.create(:pod) @@ -104,6 +114,32 @@ describe Pod, type: :model do end end + describe "#schedule_check_if_needed" do + it "schedules the pod for the next check if it is offline" do + pod = FactoryGirl.create(:pod, status: :net_failed) + pod.schedule_check_if_needed + expect(pod.scheduled_check).to be_truthy + end + + it "does nothing if the pod unchecked" do + pod = FactoryGirl.create(:pod) + pod.schedule_check_if_needed + expect(pod.scheduled_check).to be_falsey + end + + it "does nothing if the pod is online" do + pod = FactoryGirl.create(:pod, status: :no_errors) + pod.schedule_check_if_needed + expect(pod.scheduled_check).to be_falsey + end + + it "does nothing if the pod is scheduled for the next check" do + pod = FactoryGirl.create(:pod, status: :no_errors, scheduled_check: true) + expect(pod).not_to receive(:update_column) + pod.schedule_check_if_needed + end + end + describe "#test_connection!" do before do @pod = FactoryGirl.create(:pod) @@ -127,6 +163,16 @@ describe Pod, type: :model do expect(@pod.checked_at).to be_within(1.second).of Time.zone.now end + it "resets the scheduled_check flag" do + allow(@result).to receive(:error) + allow(@result).to receive(:error?) + @pod.update_column(:scheduled_check, true) + + @pod.test_connection! + + expect(@pod.scheduled_check).to be_falsey + end + it "handles a failed check" do expect(@result).to receive(:error?).at_least(:once) { true } expect(@result).to receive(:error).at_least(:once) { ConnectionTester::NetFailure.new } diff --git a/spec/workers/recheck_offline_pods_spec.rb b/spec/workers/recheck_offline_pods_spec.rb new file mode 100644 index 000000000..165dd1a47 --- /dev/null +++ b/spec/workers/recheck_offline_pods_spec.rb @@ -0,0 +1,12 @@ + +require "spec_helper" + +describe Workers::RecheckScheduledPods do + it "performs a connection test on all scheduled pods" do + (0..4).map { FactoryGirl.create(:pod) } + FactoryGirl.create(:pod, scheduled_check: true) + + expect_any_instance_of(Pod).to receive(:test_connection!) + Workers::RecheckScheduledPods.new.perform + end +end From 704e5bd399c62856ff7a1005121e6385dbec330c Mon Sep 17 00:00:00 2001 From: Benjamin Neff Date: Sun, 30 Oct 2016 23:44:35 +0100 Subject: [PATCH 14/71] Remove unused /user/getting_started_completed route The used one is /getting_started_completed closes #7164 --- config/routes.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 190ad6b98..288733244 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -102,7 +102,6 @@ Diaspora::Application.routes.draw do resource :user, only: %i(edit destroy), shallow: true do put :edit, action: :update - get :getting_started_completed post :export_profile get :download_profile post :export_photos From 851c16d80cf599756b455194444d9b982a193ba1 Mon Sep 17 00:00:00 2001 From: Benjamin Neff Date: Tue, 1 Nov 2016 22:32:38 +0100 Subject: [PATCH 15/71] Add setting for a custom changelog url. fixes #7073 closes #7166 --- app/helpers/application_helper.rb | 2 ++ config/defaults.yml | 1 + config/diaspora.yml.example | 5 +++++ spec/helpers/application_helper_spec.rb | 14 ++++++++++++++ 4 files changed, 22 insertions(+) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 85e0567fd..4a20f5909 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -12,6 +12,8 @@ module ApplicationHelper end def changelog_url + return AppConfig.settings.changelog_url.get if AppConfig.settings.changelog_url.present? + url = "https://github.com/diaspora/diaspora/blob/master/Changelog.md" url.sub!('/master/', "/#{AppConfig.git_revision}/") if AppConfig.git_revision.present? url diff --git a/config/defaults.yml b/config/defaults.yml index 9ca81debd..853e37599 100644 --- a/config/defaults.yml +++ b/config/defaults.yml @@ -144,6 +144,7 @@ defaults: warn_days: 30 limit_removals_to_per_day: 100 source_url: + changelog_url: default_color_theme: "original" default_metas: title: 'diaspora* social network' diff --git a/config/diaspora.yml.example b/config/diaspora.yml.example index 25f7291a2..93b0e014f 100644 --- a/config/diaspora.yml.example +++ b/config/diaspora.yml.example @@ -535,6 +535,11 @@ configuration: ## Section ## If not set your pod will provide a downloadable archive. #source_url: 'https://example.org/username/diaspora' + ## Changelog URL + ## URL to the changelog of the diaspora-version your pod is currently running. + ## If not set an auto-generated url to github is used. + #changelog_url: "https://github.com/diaspora/diaspora/blob/master/Changelog.md" + ## Default color theme ## You can change which color theme is displayed when a user is not signed in ## or has not selected any color theme from the available ones. You simply have diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 4c3ec1023..68ef70571 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -89,15 +89,29 @@ describe ApplicationHelper, :type => :helper do end describe "#changelog_url" do + let(:changelog_url_setting) { + double.tap {|double| allow(AppConfig).to receive(:settings).and_return(double(changelog_url: double)) } + } + it "defaults to master branch changleog" do + expect(changelog_url_setting).to receive(:present?).and_return(false) expect(AppConfig).to receive(:git_revision).and_return(nil) expect(changelog_url).to eq("https://github.com/diaspora/diaspora/blob/master/Changelog.md") end it "displays the changelog for the current git revision if set" do + expect(changelog_url_setting).to receive(:present?).and_return(false) expect(AppConfig).to receive(:git_revision).twice.and_return("123") expect(changelog_url).to eq("https://github.com/diaspora/diaspora/blob/123/Changelog.md") end + + it "displays the configured changelog url if set" do + expect(changelog_url_setting).to receive(:present?).and_return(true) + expect(changelog_url_setting).to receive(:get) + .and_return("https://github.com/diaspora/diaspora/blob/develop/Changelog.md") + expect(AppConfig).not_to receive(:git_revision) + expect(changelog_url).to eq("https://github.com/diaspora/diaspora/blob/develop/Changelog.md") + end end describe '#pod_name' do From fa329cc03200cbcfd60519f277ee8a3f8f9594ac Mon Sep 17 00:00:00 2001 From: Dennis Schubert Date: Sun, 6 Nov 2016 02:43:32 +0100 Subject: [PATCH 16/71] Add changelog note for custom changelog URL. [ci skip] --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index 0e8a75305..229fc8d47 100644 --- a/Changelog.md +++ b/Changelog.md @@ -9,6 +9,7 @@ ## Features * Show spinner when loading comments in the stream [#7170](https://github.com/diaspora/diaspora/pull/7170) * Add a dark color theme [#7152](https://github.com/diaspora/diaspora/pull/7152) +* Added setting for custom changelog URL [#7166](https://github.com/diaspora/diaspora/pull/7166) # 0.6.1.0 From b3e5bfdafdb06f97805bfc0fd66353d82f91ced9 Mon Sep 17 00:00:00 2001 From: Dennis Schubert Date: Sun, 6 Nov 2016 03:04:46 +0100 Subject: [PATCH 17/71] Set the hidden mention to transparent, not white. --- app/assets/stylesheets/mentions.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/mentions.scss b/app/assets/stylesheets/mentions.scss index 71028e629..a6920e7b3 100644 --- a/app/assets/stylesheets/mentions.scss +++ b/app/assets/stylesheets/mentions.scss @@ -75,7 +75,7 @@ } .mentions { - color: white; + color: transparent; font-size: $font-size-base; font-family: Arial, Helvetica, sans-serif; overflow: hidden; From 5269a0d3c0038a516eeb32e6f751e91084d0ddb4 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Sat, 12 Nov 2016 21:37:48 +0100 Subject: [PATCH 18/71] Fix aspect membership view spec syntax Regression introduced in #7132 --- spec/javascripts/app/views/aspect_membership_view_spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/javascripts/app/views/aspect_membership_view_spec.js b/spec/javascripts/app/views/aspect_membership_view_spec.js index 23766c7ab..270aa4173 100644 --- a/spec/javascripts/app/views/aspect_membership_view_spec.js +++ b/spec/javascripts/app/views/aspect_membership_view_spec.js @@ -59,7 +59,6 @@ describe("app.views.AspectMembership", function(){ jasmine.Ajax.requests.mostRecent().respondWith(resp_fail); expect(spec.content().find(".flash-message")).toBeErrorFlashMessage("error message"); - ); }); }); From f2fdaf1daf1c3f2119990b79d3dc578cc711e0c7 Mon Sep 17 00:00:00 2001 From: Augier Date: Fri, 30 Sep 2016 12:04:04 +0200 Subject: [PATCH 19/71] Use typeahead on conversations --- app/assets/javascripts/app/pages/contacts.js | 3 + .../app/views/conversations_form_view.js | 83 ++- .../app/views/conversations_inbox_view.js | 2 +- .../app/views/profile_header_view.js | 5 +- .../app/views/publisher/mention_view.js | 2 +- .../javascripts/app/views/search_base_view.js | 8 +- .../javascripts/app/views/search_view.js | 2 +- app/assets/stylesheets/conversations.scss | 52 +- .../stylesheets/mobile/conversations.scss | 2 + .../conversation_recipient_tag_tpl.jst.hbs | 12 + app/controllers/contacts_controller.rb | 3 +- app/controllers/conversations_controller.rb | 34 +- app/models/person.rb | 4 +- app/views/contacts/index.html.haml | 2 +- app/views/conversations/_new.haml | 14 +- app/views/conversations/new.html.haml | 12 +- app/views/conversations/new.mobile.haml | 8 +- app/views/people/show.html.haml | 2 +- .../step_definitions/conversations_steps.rb | 8 +- spec/controllers/contacts_controller_spec.rb | 11 + .../conversations_controller_spec.rb | 511 +++++++++++------- .../jasmine_fixtures/conversations_spec.rb | 3 + spec/javascripts/app/pages/contacts_spec.js | 27 + .../app/views/conversations_form_view_spec.js | 171 +++++- .../app/views/profile_header_view_spec.js | 26 + .../app/views/publisher_mention_view_spec.js | 2 +- .../javascripts/app/views/search_view_spec.js | 2 +- 27 files changed, 749 insertions(+), 262 deletions(-) create mode 100644 app/assets/templates/conversation_recipient_tag_tpl.jst.hbs diff --git a/app/assets/javascripts/app/pages/contacts.js b/app/assets/javascripts/app/pages/contacts.js index eca20fa33..f9bd2db27 100644 --- a/app/assets/javascripts/app/pages/contacts.js +++ b/app/assets/javascripts/app/pages/contacts.js @@ -79,6 +79,9 @@ app.pages.Contacts = Backbone.View.extend({ }, showMessageModal: function(){ + $("#conversationModal").on("modal:loaded", function() { + new app.views.ConversationsForm({prefill: gon.conversationPrefill}); + }); app.helpers.showModal("#conversationModal"); }, diff --git a/app/assets/javascripts/app/views/conversations_form_view.js b/app/assets/javascripts/app/views/conversations_form_view.js index bfdfe62fb..0b304739b 100644 --- a/app/assets/javascripts/app/views/conversations_form_view.js +++ b/app/assets/javascripts/app/views/conversations_form_view.js @@ -5,40 +5,83 @@ app.views.ConversationsForm = Backbone.View.extend({ events: { "keydown .conversation-message-text": "keyDown", + "click .conversation-recipient-tag .remove": "removeRecipient" }, initialize: function(opts) { - this.contacts = _.has(opts, "contacts") ? opts.contacts : null; - this.prefill = []; - if (_.has(opts, "prefillName") && _.has(opts, "prefillValue")) { - this.prefill = [{name: opts.prefillName, value: opts.prefillValue}]; + opts = opts || {}; + this.conversationRecipients = []; + + this.typeaheadElement = this.$el.find("#contacts-search-input"); + this.contactsIdsListInput = this.$el.find("#contact-ids"); + this.tagListElement = this.$("#recipients-tag-list"); + + this.search = new app.views.SearchBase({ + el: this.$el.find("#new-conversation"), + typeaheadInput: this.typeaheadElement, + customSearch: true, + autoselect: true, + remoteRoute: {url: "/contacts", extraParameters: "mutual=true"} + }); + + this.bindTypeaheadEvents(); + + this.tagListElement.empty(); + if (opts.prefill) { + this.prefill(opts.prefill); } - this.prepareAutocomplete(this.contacts); + this.$("form#new-conversation").on("ajax:success", this.conversationCreateSuccess); this.$("form#new-conversation").on("ajax:error", this.conversationCreateError); }, - prepareAutocomplete: function(data){ - this.$("#contact-autocomplete").autoSuggest(data, { - selectedItemProp: "name", - searchObjProps: "name", - asHtmlID: "contact_ids", - retrieveLimit: 10, - minChars: 1, - keyDelay: 0, - startText: '', - emptyText: Diaspora.I18n.t("no_results"), - preFill: this.prefill - }); - $("#contact_ids").attr("aria-labelledby", "toLabel").focus(); + addRecipient: function(person) { + this.conversationRecipients.push(person); + this.updateContactIdsListInput(); + /* eslint-disable camelcase */ + this.tagListElement.append(HandlebarsTemplates.conversation_recipient_tag_tpl(person)); + /* eslint-enable camelcase */ }, - keyDown : function(evt) { - if(evt.which === Keycodes.ENTER && evt.ctrlKey) { + prefill: function(handles) { + handles.forEach(this.addRecipient.bind(this)); + }, + + updateContactIdsListInput: function() { + this.contactsIdsListInput.val(_(this.conversationRecipients).pluck("id").join(",")); + this.search.ignoreDiasporaIds.length = 0; + this.conversationRecipients.forEach(this.search.ignorePersonForSuggestions.bind(this.search)); + }, + + bindTypeaheadEvents: function() { + this.typeaheadElement.on("typeahead:select", function(evt, person) { + this.onSuggestionSelection(person); + }.bind(this)); + }, + + onSuggestionSelection: function(person) { + this.addRecipient(person); + this.typeaheadElement.typeahead("val", ""); + }, + + keyDown: function(evt) { + if (evt.which === Keycodes.ENTER && evt.ctrlKey) { $(evt.target).parents("form").submit(); } }, + removeRecipient: function(evt) { + var $recipientTagEl = $(evt.target).parents(".conversation-recipient-tag"); + var diasporaHandle = $recipientTagEl.data("diaspora-handle"); + + this.conversationRecipients = this.conversationRecipients.filter(function(person) { + return diasporaHandle.localeCompare(person.handle) !== 0; + }); + + this.updateContactIdsListInput(); + $recipientTagEl.remove(); + }, + conversationCreateSuccess: function(evt, data) { app._changeLocation(Routes.conversation(data.id)); }, diff --git a/app/assets/javascripts/app/views/conversations_inbox_view.js b/app/assets/javascripts/app/views/conversations_inbox_view.js index 97e31f5c3..4e301a8a0 100644 --- a/app/assets/javascripts/app/views/conversations_inbox_view.js +++ b/app/assets/javascripts/app/views/conversations_inbox_view.js @@ -9,7 +9,7 @@ app.views.ConversationsInbox = Backbone.View.extend({ }, initialize: function() { - new app.views.ConversationsForm({contacts: gon.contacts}); + new app.views.ConversationsForm(); this.setupConversation(); }, diff --git a/app/assets/javascripts/app/views/profile_header_view.js b/app/assets/javascripts/app/views/profile_header_view.js index 906736895..0fc671b8e 100644 --- a/app/assets/javascripts/app/views/profile_header_view.js +++ b/app/assets/javascripts/app/views/profile_header_view.js @@ -79,8 +79,11 @@ app.views.ProfileHeader = app.views.Base.extend({ }, showMessageModal: function(){ + $("#conversationModal").on("modal:loaded", function() { + new app.views.ConversationsForm({prefill: gon.conversationPrefill}); + }); app.helpers.showModal("#conversationModal"); - }, + } }); // @license-end diff --git a/app/assets/javascripts/app/views/publisher/mention_view.js b/app/assets/javascripts/app/views/publisher/mention_view.js index 75c0015ea..86b3b07aa 100644 --- a/app/assets/javascripts/app/views/publisher/mention_view.js +++ b/app/assets/javascripts/app/views/publisher/mention_view.js @@ -32,7 +32,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({ typeaheadInput: this.typeaheadInput, customSearch: true, autoselect: true, - remoteRoute: "/contacts" + remoteRoute: {url: "/contacts"} }); }, diff --git a/app/assets/javascripts/app/views/search_base_view.js b/app/assets/javascripts/app/views/search_base_view.js index 44277b851..eb00aaeaa 100644 --- a/app/assets/javascripts/app/views/search_base_view.js +++ b/app/assets/javascripts/app/views/search_base_view.js @@ -28,9 +28,13 @@ app.views.SearchBase = app.views.Base.extend({ }; // Allow bloodhound to look for remote results if there is a route given in the options - if(options.remoteRoute) { + if (options.remoteRoute && options.remoteRoute.url) { + var extraParameters = ""; + if (options.remoteRoute.extraParameters) { + extraParameters += "&" + options.remoteRoute.extraParameters; + } bloodhoundOptions.remote = { - url: options.remoteRoute + ".json?q=%QUERY", + url: options.remoteRoute.url + ".json?q=%QUERY" + extraParameters, wildcard: "%QUERY", transform: this.transformBloodhoundResponse.bind(this) }; diff --git a/app/assets/javascripts/app/views/search_view.js b/app/assets/javascripts/app/views/search_view.js index b2486659b..c0d8ec749 100644 --- a/app/assets/javascripts/app/views/search_view.js +++ b/app/assets/javascripts/app/views/search_view.js @@ -10,7 +10,7 @@ app.views.Search = app.views.SearchBase.extend({ this.searchInput = this.$("#q"); app.views.SearchBase.prototype.initialize.call(this, { typeaheadInput: this.searchInput, - remoteRoute: this.$el.attr("action"), + remoteRoute: {url: this.$el.attr("action")}, suggestionLink: true }); this.searchInput.on("typeahead:select", this.suggestionSelected); diff --git a/app/assets/stylesheets/conversations.scss b/app/assets/stylesheets/conversations.scss index dd443d646..8c1216cd5 100644 --- a/app/assets/stylesheets/conversations.scss +++ b/app/assets/stylesheets/conversations.scss @@ -183,10 +183,60 @@ } // scss-lint:enable SelectorDepth -#new_conversation_pane { +.new-conversation { ul.as-selections { width: 100% !important; } + input#contact_ids { box-shadow: none; } + label { font-weight: bold; } + + .twitter-typeahead, + .tt-menu { + width: 100%; + } +} + +.recipients-tag-list { + .conversation-recipient-tag { + background-color: $brand-primary; + border-radius: $btn-border-radius-base; + display: inline-flex; + margin: 0 2px $form-group-margin-bottom; + padding: 8px; + + &:first-child { margin-left: 0; } + + &:last-child { margin-right: 0; } + + div { + align-self: center; + justify-content: flex-start; + } + } + + .avatar { + height: 40px; + margin-right: 8px; + width: 40px; + } + + .name-and-handle { + color: $white; + margin-right: 8px; + text-align: left; + + .diaspora-id { font-size: $font-size-small; } + } + + .entypo-circled-cross { + color: $white; + cursor: pointer; + font-size: 20px; + height: 22px; + line-height: 22px; + + &:hover { color: $light-grey; } + } } .new-conversation.form-horizontal .form-group:last-of-type { margin-bottom: 0; } diff --git a/app/assets/stylesheets/mobile/conversations.scss b/app/assets/stylesheets/mobile/conversations.scss index e0e02411a..ef3409f79 100644 --- a/app/assets/stylesheets/mobile/conversations.scss +++ b/app/assets/stylesheets/mobile/conversations.scss @@ -61,3 +61,5 @@ .subject { padding: 0 10px; } .message-count, .unread-message-count { margin: 10px 2px; } + +.new-conversation .as-selections { background-color: transparent; } diff --git a/app/assets/templates/conversation_recipient_tag_tpl.jst.hbs b/app/assets/templates/conversation_recipient_tag_tpl.jst.hbs new file mode 100644 index 000000000..296c0b7f9 --- /dev/null +++ b/app/assets/templates/conversation_recipient_tag_tpl.jst.hbs @@ -0,0 +1,12 @@ +
+
+ +
+
+
{{ name }}
+
{{ handle }}
+
+
+ +
+
diff --git a/app/controllers/contacts_controller.rb b/app/controllers/contacts_controller.rb index a0be1dcec..ed5999f35 100644 --- a/app/controllers/contacts_controller.rb +++ b/app/controllers/contacts_controller.rb @@ -17,7 +17,8 @@ class ContactsController < ApplicationController # Used for mentions in the publisher and pagination on the contacts page format.json { @people = if params[:q].present? - Person.search(params[:q], current_user, only_contacts: true).limit(15) + mutual = params[:mutual].present? && params[:mutual] + Person.search(params[:q], current_user, only_contacts: true, mutual: mutual).limit(15) else set_up_contacts_json end diff --git a/app/controllers/conversations_controller.rb b/app/controllers/conversations_controller.rb index ba99b6b6a..4512e8a3c 100644 --- a/app/controllers/conversations_controller.rb +++ b/app/controllers/conversations_controller.rb @@ -30,12 +30,12 @@ class ConversationsController < ApplicationController end def create - contact_ids = params[:contact_ids] - - # Can't split nil - if contact_ids - contact_ids = contact_ids.split(',') if contact_ids.is_a? String - person_ids = current_user.contacts.where(id: contact_ids).pluck(:person_id) + # Contacts autocomplete does not work the same way on mobile and desktop + # Mobile returns contact ids array while desktop returns person id + # This will have to be removed when mobile autocomplete is ported to Typeahead + recipients_param, column = [%i(contact_ids id), %i(person_ids person_id)].find {|param, _| params[param].present? } + if recipients_param + person_ids = current_user.contacts.where(column => params[recipients_param].split(",")).pluck(:person_id) end opts = params.require(:conversation).permit(:subject) @@ -91,17 +91,23 @@ class ConversationsController < ApplicationController return end - @contacts_json = contacts_data.to_json - @contact_ids = "" - - if params[:contact_id] - @contact_ids = current_user.contacts.find(params[:contact_id]).id - elsif params[:aspect_id] - @contact_ids = current_user.aspects.find(params[:aspect_id]).contacts.map{|c| c.id}.join(',') - end if session[:mobile_view] == true && request.format.html? + @contacts_json = contacts_data.to_json + + @contact_ids = if params[:contact_id] + current_user.contacts.find(params[:contact_id]).id + elsif params[:aspect_id] + current_user.aspects.find(params[:aspect_id]).contacts.pluck(:id).join(",") + end + render :layout => true else + if params[:contact_id] + gon.push conversation_prefill: [current_user.contacts.find(params[:contact_id]).person.as_json] + elsif params[:aspect_id] + gon.push conversation_prefill: current_user.aspects + .find(params[:aspect_id]).contacts.map {|c| c.person.as_json } + end render :layout => false end end diff --git a/app/models/person.rb b/app/models/person.rb index 35673144b..8e0d5b6e2 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -145,7 +145,7 @@ class Person < ActiveRecord::Base [where_clause, q_tokens] end - def self.search(search_str, user, only_contacts: false) + def self.search(search_str, user, only_contacts: false, mutual: false) search_str.strip! return none if search_str.blank? || search_str.size < 2 @@ -159,6 +159,8 @@ class Person < ActiveRecord::Base ).searchable(user) end + query = query.where(contacts: {sharing: true, receiving: true}) if mutual + query.where(closed_account: false) .where(sql, *tokens) .includes(:profile) diff --git a/app/views/contacts/index.html.haml b/app/views/contacts/index.html.haml index dffe8db21..df7a58a74 100644 --- a/app/views/contacts/index.html.haml +++ b/app/views/contacts/index.html.haml @@ -34,7 +34,7 @@ .spinner -if @aspect - #new_conversation_pane + .conversations-form-container#new_conversation_pane = render 'shared/modal', :path => new_conversation_path(:aspect_id => @aspect.id, :name => @aspect.name, :modal => true), :title => t('conversations.index.new_conversation'), diff --git a/app/views/conversations/_new.haml b/app/views/conversations/_new.haml index 41fb777aa..c84a6b95a 100644 --- a/app/views/conversations/_new.haml +++ b/app/views/conversations/_new.haml @@ -2,9 +2,13 @@ = form_for Conversation.new, html: {id: "new-conversation", class: "new-conversation form-horizontal"}, remote: true do |conversation| .form-group - %label#toLabel{for: "contact_ids"} - = t(".to") - = text_field_tag "contact_autocomplete", nil, id: "contact-autocomplete", class: "form-control" + %label#to-label{for: "contacts-search-input"}= t(".to") + .recipients-tag-list.clearfix#recipients-tag-list + = text_field_tag "contact_autocomplete", nil, id: "contacts-search-input", class: "form-control" + - unless defined?(mobile) && mobile + = text_field_tag "person_ids", nil, id: "contact-ids", type: "hidden", + aria: {labelledby: "to-label"} + .form-group %label#subject-label{for: "conversation-subject"} = t(".subject") @@ -14,12 +18,14 @@ aria: {labelledby: "subject-label"}, value: "", placeholder: t("conversations.new.subject_default") + .form-group - %label.sr-only#message-label{for: "new-message-text"} = t(".message") + %label.sr-only#message-label{for: "new-message-text"}= t(".message") = text_area_tag "conversation[text]", "", rows: 5, id: "new-message-text", class: "conversation-message-text input-block-level form-control", aria: {labelledby: "message-label"} + .form-group = conversation.submit t(".send"), "data-disable-with" => t(".sending"), :class => "btn btn-primary pull-right" diff --git a/app/views/conversations/new.html.haml b/app/views/conversations/new.html.haml index 1acbba809..e66a929dd 100644 --- a/app/views/conversations/new.html.haml +++ b/app/views/conversations/new.html.haml @@ -1,12 +1,2 @@ -:javascript - $(document).ready(function () { - var data = $.parseJSON( "#{escape_javascript(@contacts_json)}" ); - new app.views.ConversationsForm({ - el: $("form#new-conversation").parent(), - contacts: data, - prefillName: "#{h params[:name]}", - prefillValue: "#{@contact_ids}" - }); - }); - += include_gon camel_case: true = render 'conversations/new' diff --git a/app/views/conversations/new.mobile.haml b/app/views/conversations/new.mobile.haml index ba78c558f..1da58fcfa 100644 --- a/app/views/conversations/new.mobile.haml +++ b/app/views/conversations/new.mobile.haml @@ -6,7 +6,7 @@ :plain $(document).ready(function () { var data = $.parseJSON( "#{escape_javascript(@contacts_json).html_safe}" ), - autocompleteInput = $("#contact-autocomplete"); + autocompleteInput = $("#contacts-search-input"); autocompleteInput.autoSuggest(data, { selectedItemProp: "name", @@ -15,7 +15,7 @@ retrieveLimit: 10, minChars: 1, keyDelay: 0, - startText: '', + startText: "", emptyText: "#{t("no_results")}", preFill: [{name : "#{h params[:name]}", value : "#{@contact_ids}"}] @@ -27,6 +27,6 @@ #flash-messages .container-fluid.row %h3 - = t('conversations.index.new_conversation') + = t("conversations.index.new_conversation") - = render 'conversations/new' + = render "conversations/new", mobile: true diff --git a/app/views/people/show.html.haml b/app/views/people/show.html.haml index 0eae5e336..60e707010 100644 --- a/app/views/people/show.html.haml +++ b/app/views/people/show.html.haml @@ -40,7 +40,7 @@ id: 'mentionModal' -if @contact - #new_conversation_pane + .conversations-form-container#new_conversation_pane = render 'shared/modal', path: new_conversation_path(:contact_id => @contact.id, name: @contact.person.name, modal: true), title: t('conversations.index.new_conversation'), diff --git a/features/step_definitions/conversations_steps.rb b/features/step_definitions/conversations_steps.rb index 1e7bace8b..ea076b30e 100644 --- a/features/step_definitions/conversations_steps.rb +++ b/features/step_definitions/conversations_steps.rb @@ -15,8 +15,8 @@ end Then /^I send a message with subject "([^"]*)" and text "([^"]*)" to "([^"]*)"$/ do |subject, text, person| step %(I am on the conversations page) within("#new-conversation", match: :first) do - step %(I fill in "contact_autocomplete" with "#{person}") - step %(I press the first ".as-result-item" within ".as-results") + find("#contacts-search-input").native.send_key(person.to_s) + step %(I press the first ".tt-suggestion" within ".twitter-typeahead") step %(I fill in "conversation-subject" with "#{subject}") step %(I fill in "new-message-text" with "#{text}") step %(I press "Send") @@ -26,8 +26,8 @@ end Then /^I send a message with subject "([^"]*)" and text "([^"]*)" to "([^"]*)" using keyboard shortcuts$/ do |subject, text, person| step %(I am on the conversations page) within("#new-conversation", match: :first) do - step %(I fill in "contact_autocomplete" with "#{person}") - step %(I press the first ".as-result-item" within ".as-results") + find("#contacts-search-input").native.send_key(person.to_s) + step %(I press the first ".tt-suggestion" within ".twitter-typeahead") step %(I fill in "conversation-subject" with "#{subject}") step %(I fill in "new-message-text" with "#{text}") find("#new-message-text").native.send_key %i(Ctrl Return) diff --git a/spec/controllers/contacts_controller_spec.rb b/spec/controllers/contacts_controller_spec.rb index 7a8ab7a13..8b7723b25 100644 --- a/spec/controllers/contacts_controller_spec.rb +++ b/spec/controllers/contacts_controller_spec.rb @@ -37,6 +37,8 @@ describe ContactsController, :type => :controller do @person1 = FactoryGirl.create(:person) bob.share_with(@person1, bob.aspects.first) @person2 = FactoryGirl.create(:person) + @person3 = FactoryGirl.create(:person) + bob.contacts.create(person: @person3, aspects: [bob.aspects.first], receiving: true, sharing: true) end it "succeeds" do @@ -53,6 +55,15 @@ describe ContactsController, :type => :controller do get :index, q: @person2.first_name, format: "json" expect(response.body).to eq([].to_json) end + + it "only returns mutual contacts when mutual parameter is true" do + get :index, q: @person1.first_name, mutual: true, format: "json" + expect(response.body).to eq([].to_json) + get :index, q: @person2.first_name, mutual: true, format: "json" + expect(response.body).to eq([].to_json) + get :index, q: @person3.first_name, mutual: true, format: "json" + expect(response.body).to eq([@person3].to_json) + end end context "for pagination on the contacts page" do diff --git a/spec/controllers/conversations_controller_spec.rb b/spec/controllers/conversations_controller_spec.rb index 0002468de..86709b263 100644 --- a/spec/controllers/conversations_controller_spec.rb +++ b/spec/controllers/conversations_controller_spec.rb @@ -16,48 +16,57 @@ describe ConversationsController, :type => :controller do end end - describe '#new modal' do - it 'succeeds' do - get :new, :modal => true - expect(response).to be_success - end + describe "#new modal" do + context "desktop and mobile" do + it "succeeds" do + get :new, modal: true + expect(response).to be_success + end - it "assigns a json list of contacts that are sharing with the person" do - sharing_user = FactoryGirl.create(:user_with_aspect) - sharing_user.share_with(alice.person, sharing_user.aspects.first) - get :new, :modal => true - expect(assigns(:contacts_json)).to include(alice.contacts.where(sharing: true, receiving: true).first.person.name) - alice.contacts << Contact.new(:person_id => eve.person.id, :user_id => alice.id, :sharing => false, :receiving => true) - expect(assigns(:contacts_json)).not_to include(alice.contacts.where(sharing: false).first.person.name) - expect(assigns(:contacts_json)).not_to include(alice.contacts.where(receiving: false).first.person.name) - end + it "assigns a contact if passed a contact id" do + get :new, contact_id: alice.contacts.first.id, modal: true + expect(controller.gon.conversation_prefill).to eq([alice.contacts.first.person.as_json]) + end - it "assigns a contact if passed a contact id" do - get :new, :contact_id => alice.contacts.first.id, :modal => true - expect(assigns(:contact_ids)).to eq(alice.contacts.first.id) - end + it "assigns a set of contacts if passed an aspect id" do + get :new, aspect_id: alice.aspects.first.id, modal: true + expect(controller.gon.conversation_prefill).to eq(alice.aspects.first.contacts.map {|c| c.person.as_json }) + end - it "assigns a set of contacts if passed an aspect id" do - get :new, :aspect_id => alice.aspects.first.id, :modal => true - expect(assigns(:contact_ids)).to eq(alice.aspects.first.contacts.map(&:id).join(',')) - end - - it "does not allow XSS via the name parameter" do - ["", - '"}]});alert(1);(function f() {var foo = [{b:"'].each do |xss| - get :new, :modal => true, name: xss - expect(response.body).not_to include xss + it "does not allow XSS via the name parameter" do + ["", + '"}]});alert(1);(function f() {var foo = [{b:"'].each do |xss| + get :new, modal: true, name: xss + expect(response.body).not_to include xss + end end end - it "does not allow XSS via the profile name" do - xss = "" - contact = alice.contacts.first - contact.person.profile.update_attribute(:first_name, xss) - get :new, :modal => true - json = JSON.parse(assigns(:contacts_json)).first - expect(json['value'].to_s).to eq(contact.id.to_s) - expect(json['name']).to_not include(xss) + context "mobile" do + before do + controller.session[:mobile_view] = true + end + + it "assigns a json list of contacts that are sharing with the person" do + sharing_user = FactoryGirl.create(:user_with_aspect) + sharing_user.share_with(alice.person, sharing_user.aspects.first) + get :new, modal: true + expect(assigns(:contacts_json)) + .to include(alice.contacts.where(sharing: true, receiving: true).first.person.name) + alice.contacts << Contact.new(person_id: eve.person.id, user_id: alice.id, sharing: false, receiving: true) + expect(assigns(:contacts_json)).not_to include(alice.contacts.where(sharing: false).first.person.name) + expect(assigns(:contacts_json)).not_to include(alice.contacts.where(receiving: false).first.person.name) + end + + it "does not allow XSS via the profile name" do + xss = "" + contact = alice.contacts.first + contact.person.profile.update_attribute(:first_name, xss) + get :new, modal: true + json = JSON.parse(assigns(:contacts_json)).first + expect(json["value"].to_s).to eq(contact.id.to_s) + expect(json["name"]).to_not include(xss) + end end end @@ -115,203 +124,323 @@ describe ConversationsController, :type => :controller do end end - describe '#create' do - context 'with a valid conversation' do - before do - @hash = { - :format => :js, - :conversation => { - :subject => "secret stuff", - :text => 'text debug' - }, - :contact_ids => [alice.contacts.first.id] - } - end - - it 'creates a conversation' do - expect { - post :create, @hash - }.to change(Conversation, :count).by(1) - end - - it 'creates a message' do - expect { - post :create, @hash - }.to change(Message, :count).by(1) - end - - it "responds with the conversation id as JSON" do - post :create, @hash - expect(response).to be_success - expect(JSON.parse(response.body)["id"]).to eq(Conversation.first.id) - end - - it 'sets the author to the current_user' do - @hash[:author] = FactoryGirl.create(:user) - post :create, @hash - expect(Message.first.author).to eq(alice.person) - expect(Conversation.first.author).to eq(alice.person) - end - - it 'dispatches the conversation' do - cnv = Conversation.create( - { - :author => alice.person, - :participant_ids => [alice.contacts.first.person.id, alice.person.id], - :subject => 'not spam', - :messages_attributes => [ {:author => alice.person, :text => 'cool stuff'} ] + describe "#create" do + context "desktop" do + context "with a valid conversation" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + person_ids: [alice.contacts.first.person.id] } - ) + end - expect(Diaspora::Federation::Dispatcher).to receive(:defer_dispatch) - post :create, @hash - end - end + it "creates a conversation" do + expect { post :create, @hash }.to change(Conversation, :count).by(1) + end - context 'with empty subject' do - before do - @hash = { - :format => :js, - :conversation => { - :subject => ' ', - :text => 'text debug' - }, - :contact_ids => [alice.contacts.first.id] - } - end + it "creates a message" do + expect { post :create, @hash }.to change(Message, :count).by(1) + end - it 'creates a conversation' do - expect { + it "responds with the conversation id as JSON" do post :create, @hash - }.to change(Conversation, :count).by(1) - end + expect(response).to be_success + expect(JSON.parse(response.body)["id"]).to eq(Conversation.first.id) + end - it 'creates a message' do - expect { + it "sets the author to the current_user" do + @hash[:author] = FactoryGirl.create(:user) post :create, @hash - }.to change(Message, :count).by(1) + expect(Message.first.author).to eq(alice.person) + expect(Conversation.first.author).to eq(alice.person) + end + + it "dispatches the conversation" do + Conversation.create(author: alice.person, participant_ids: [alice.contacts.first.person.id, alice.person.id], + subject: "not spam", messages_attributes: [{author: alice.person, text: "cool stuff"}]) + + expect(Diaspora::Federation::Dispatcher).to receive(:defer_dispatch) + post :create, @hash + end end - it "responds with the conversation id as JSON" do - post :create, @hash - expect(response).to be_success - expect(JSON.parse(response.body)["id"]).to eq(Conversation.first.id) + context "with empty subject" do + before do + @hash = { + format: :js, + conversation: {subject: " ", text: "text debug"}, + person_ids: [alice.contacts.first.person.id] + } + end + + it "creates a conversation" do + expect { post :create, @hash }.to change(Conversation, :count).by(1) + end + + it "creates a message" do + expect { post :create, @hash }.to change(Message, :count).by(1) + end + + it "responds with the conversation id as JSON" do + post :create, @hash + expect(response).to be_success + expect(JSON.parse(response.body)["id"]).to eq(Conversation.first.id) + end + end + + context "with empty text" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: " "}, + person_ids: [alice.contacts.first.person.id] + } + end + + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end + + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end + + it "responds with an error message" do + post :create, @hash + expect(response).not_to be_success + expect(response.body).to eq(I18n.t("conversations.create.fail")) + end + end + + context "with empty contact" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + person_ids: " " + } + end + + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end + + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end + + it "responds with an error message" do + post :create, @hash + expect(response).not_to be_success + expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) + end + end + + context "with nil contact" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + person_ids: nil + } + end + + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end + + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end + + it "responds with an error message" do + post :create, @hash + expect(response).not_to be_success + expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) + end end end - context 'with empty text' do + context "mobile" do before do - @hash = { - :format => :js, - :conversation => { - :subject => 'secret stuff', - :text => ' ' - }, - :contact_ids => [alice.contacts.first.id] - } + controller.session[:mobile_view] = true end - it 'does not create a conversation' do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) + context "with a valid conversation" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + contact_ids: [alice.contacts.first.id] + } + end + + it "creates a conversation" do + expect { post :create, @hash }.to change(Conversation, :count).by(1) + end + + it "creates a message" do + expect { post :create, @hash }.to change(Message, :count).by(1) + end + + it "responds with the conversation id as JSON" do + post :create, @hash + expect(response).to be_success + expect(JSON.parse(response.body)["id"]).to eq(Conversation.first.id) + end + + it "sets the author to the current_user" do + @hash[:author] = FactoryGirl.create(:user) + post :create, @hash + expect(Message.first.author).to eq(alice.person) + expect(Conversation.first.author).to eq(alice.person) + end + + it "dispatches the conversation" do + Conversation.create(author: alice.person, participant_ids: [alice.contacts.first.person.id, alice.person.id], + subject: "not spam", messages_attributes: [{author: alice.person, text: "cool stuff"}]) + + expect(Diaspora::Federation::Dispatcher).to receive(:defer_dispatch) + post :create, @hash + end end - it 'does not create a message' do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) + context "with empty subject" do + before do + @hash = { + format: :js, + conversation: {subject: " ", text: "text debug"}, + contact_ids: [alice.contacts.first.id] + } + end + + it "creates a conversation" do + expect { post :create, @hash }.to change(Conversation, :count).by(1) + end + + it "creates a message" do + expect { post :create, @hash }.to change(Message, :count).by(1) + end + + it "responds with the conversation id as JSON" do + post :create, @hash + expect(response).to be_success + expect(JSON.parse(response.body)["id"]).to eq(Conversation.first.id) + end end - it "responds with an error message" do - post :create, @hash - expect(response).not_to be_success - expect(response.body).to eq(I18n.t("conversations.create.fail")) - end - end + context "with empty text" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: " "}, + contact_ids: [alice.contacts.first.id] + } + end - context 'with empty contact' do - before do - @hash = { - :format => :js, - :conversation => { - :subject => 'secret stuff', - :text => 'text debug' - }, - :contact_ids => ' ' - } + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end + + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end + + it "responds with an error message" do + post :create, @hash + expect(response).not_to be_success + expect(response.body).to eq(I18n.t("conversations.create.fail")) + end end - it 'does not create a conversation' do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) + context "with empty contact" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + contact_ids: " " + } + end + + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end + + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end + + it "responds with an error message" do + post :create, @hash + expect(response).not_to be_success + expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) + end end - it 'does not create a message' do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) - end + context "with nil contact" do + before do + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + contact_ids: nil + } + end - it "responds with an error message" do - post :create, @hash - expect(response).not_to be_success - expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) - end - end + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end - context 'with nil contact' do - before do - @hash = { - :format => :js, - :conversation => { - :subject => 'secret stuff', - :text => 'text debug' - }, - :contact_ids => nil - } - end - - it 'does not create a conversation' do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) - end - - it 'does not create a message' do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) - end - - it "responds with an error message" do - post :create, @hash - expect(response).not_to be_success - expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end end end end - describe '#show' do + describe "#show" do before do hash = { - :author => alice.person, - :participant_ids => [alice.contacts.first.person.id, alice.person.id], - :subject => 'not spam', - :messages_attributes => [ {:author => alice.person, :text => 'cool stuff'} ] + author: alice.person, + participant_ids: [alice.contacts.first.person.id, alice.person.id], + subject: "not spam", + messages_attributes: [{author: alice.person, text: "cool stuff"}] } @conversation = Conversation.create(hash) end - it 'succeeds with json' do + it "succeeds with json" do get :show, :id => @conversation.id, :format => :json expect(response).to be_success expect(assigns[:conversation]).to eq(@conversation) expect(response.body).to include @conversation.guid end - it 'redirects to index' do + it "redirects to index" do get :show, :id => @conversation.id expect(response).to redirect_to(conversations_path(:conversation_id => @conversation.id)) end diff --git a/spec/controllers/jasmine_fixtures/conversations_spec.rb b/spec/controllers/jasmine_fixtures/conversations_spec.rb index c07360d69..ac74bb7f8 100644 --- a/spec/controllers/jasmine_fixtures/conversations_spec.rb +++ b/spec/controllers/jasmine_fixtures/conversations_spec.rb @@ -29,6 +29,9 @@ describe ConversationsController, :type => :controller do get :index, :conversation_id => @conv1.id save_fixture(html_for("body"), "conversations_read") + + get :new, modal: true + save_fixture(response.body, "conversations_modal") end end diff --git a/spec/javascripts/app/pages/contacts_spec.js b/spec/javascripts/app/pages/contacts_spec.js index a40b899d2..8574e6a5f 100644 --- a/spec/javascripts/app/pages/contacts_spec.js +++ b/spec/javascripts/app/pages/contacts_spec.js @@ -277,4 +277,31 @@ describe("app.pages.Contacts", function(){ }); }); }); + + describe("showMessageModal", function() { + beforeEach(function() { + $("body").append("
").append(spec.readFixture("conversations_modal")); + }); + + it("calls app.helpers.showModal", function() { + spyOn(app.helpers, "showModal"); + this.view.showMessageModal(); + expect(app.helpers.showModal); + }); + + it("app.views.ConversationsForm with correct parameters when modal is loaded", function() { + gon.conversationPrefill = [ + {id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}, + {id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}, + {id: 3, name: "user@pod.tld", handle: "user@pod.tld"} + ]; + + spyOn(app.views.ConversationsForm.prototype, "initialize"); + this.view.showMessageModal(); + $("#conversationModal").trigger("modal:loaded"); + expect($("#conversationModal").length).toBe(1); + expect(app.views.ConversationsForm.prototype.initialize) + .toHaveBeenCalledWith({prefill: gon.conversationPrefill}); + }); + }); }); diff --git a/spec/javascripts/app/views/conversations_form_view_spec.js b/spec/javascripts/app/views/conversations_form_view_spec.js index 989469670..97760a067 100644 --- a/spec/javascripts/app/views/conversations_form_view_spec.js +++ b/spec/javascripts/app/views/conversations_form_view_spec.js @@ -1,12 +1,133 @@ describe("app.views.ConversationsForm", function() { beforeEach(function() { spec.loadFixture("conversations_read"); + this.target = new app.views.ConversationsForm(); + }); + + describe("initialize", function() { + it("initializes the conversation participants list", function() { + expect(this.target.conversationRecipients).toEqual([]); + }); + + it("initializes the search view", function() { + spyOn(app.views.SearchBase.prototype, "initialize"); + this.target.initialize(); + expect(app.views.SearchBase.prototype.initialize).toHaveBeenCalled(); + expect(app.views.SearchBase.prototype.initialize.calls.argsFor(0)[0].customSearch).toBe(true); + expect(app.views.SearchBase.prototype.initialize.calls.argsFor(0)[0].autoselect).toBe(true); + expect(app.views.SearchBase.prototype.initialize.calls.argsFor(0)[0].remoteRoute).toEqual({ + url: "/contacts", + extraParameters: "mutual=true" + }); + expect(this.target.search).toBeDefined(); + }); + + it("calls bindTypeaheadEvents", function() { + spyOn(app.views.ConversationsForm.prototype, "bindTypeaheadEvents"); + this.target.initialize(); + expect(app.views.ConversationsForm.prototype.bindTypeaheadEvents).toHaveBeenCalled(); + }); + + it("calls prefill correctly", function() { + spyOn(app.views.ConversationsForm.prototype, "prefill"); + this.target.initialize(); + expect(app.views.ConversationsForm.prototype.prefill).not.toHaveBeenCalled(); + this.target.initialize({prefill: {}}); + expect(app.views.ConversationsForm.prototype.prefill).toHaveBeenCalledWith({}); + }); + }); + + describe("addRecipient", function() { + beforeEach(function() { + $("#conversation-new").removeClass("hidden"); + $("#conversation-show").addClass("hidden"); + }); + + it("add the participant", function() { + expect(this.target.conversationRecipients).toEqual([]); + this.target.addRecipient({name: "diaspora user", handle: "diaspora-user@pod.tld"}); + expect(this.target.conversationRecipients).toEqual([{name: "diaspora user", handle: "diaspora-user@pod.tld"}]); + }); + + it("call updateContactIdsListInput", function() { + spyOn(app.views.ConversationsForm.prototype, "updateContactIdsListInput"); + this.target.addRecipient({name: "diaspora user", handle: "diaspora-user@pod.tld"}); + expect(app.views.ConversationsForm.prototype.updateContactIdsListInput).toHaveBeenCalled(); + }); + + it("adds a recipient tag", function() { + expect($(".conversation-recipient-tag").length).toBe(0); + this.target.addRecipient({name: "diaspora user", handle: "diaspora-user@pod.tld"}); + expect($(".conversation-recipient-tag").length).toBe(1); + }); + }); + + describe("prefill", function() { + beforeEach(function() { + this.prefills = [{name: "diaspora user"}, {name: "other diaspora user"}, {name: "user"}]; + }); + + it("call addRecipient for each prefilled participant", function() { + spyOn(app.views.ConversationsForm.prototype, "addRecipient"); + this.target.prefill(this.prefills); + expect(app.views.ConversationsForm.prototype.addRecipient).toHaveBeenCalledTimes(this.prefills.length); + var allArgsFlattened = app.views.ConversationsForm.prototype.addRecipient.calls.allArgs().map(function(arg) { + return arg[0]; + }); + expect(allArgsFlattened).toEqual(this.prefills); + }); + }); + + describe("updateContactIdsListInput", function() { + beforeEach(function() { + this.target.conversationRecipients.push({id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}); + this.target.conversationRecipients + .push({id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}); + this.target.conversationRecipients.push({id: 3, name: "user@pod.tld", handle: "user@pod.tld"}); + }); + + it("updates hidden input value", function() { + this.target.updateContactIdsListInput(); + expect(this.target.contactsIdsListInput.val()).toBe("1,2,3"); + }); + + it("calls app.views.SearchBase.ignorePersonForSuggestions() for each participant", function() { + spyOn(app.views.SearchBase.prototype, "ignorePersonForSuggestions"); + this.target.updateContactIdsListInput(); + expect(app.views.SearchBase.prototype.ignorePersonForSuggestions).toHaveBeenCalledTimes(3); + expect(app.views.SearchBase.prototype.ignorePersonForSuggestions.calls.argsFor(0)[0]) + .toEqual({id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}); + expect(app.views.SearchBase.prototype.ignorePersonForSuggestions.calls.argsFor(1)[0]) + .toEqual({id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}); + expect(app.views.SearchBase.prototype.ignorePersonForSuggestions.calls.argsFor(2)[0]) + .toEqual({id: 3, name: "user@pod.tld", handle: "user@pod.tld"}); + }); + }); + + describe("bindTypeaheadEvents", function() { + it("calls onSuggestionSelection() when clicking on a result", function() { + spyOn(app.views.ConversationsForm.prototype, "onSuggestionSelection"); + var event = $.Event("typeahead:select"); + var person = {name: "diaspora user"}; + this.target.typeaheadElement.trigger(event, [person]); + expect(app.views.ConversationsForm.prototype.onSuggestionSelection).toHaveBeenCalledWith(person); + }); + }); + + describe("onSuggestionSelection", function() { + it("calls addRecipient, updateContactIdsListInput and $.fn.typeahead", function() { + spyOn(app.views.ConversationsForm.prototype, "addRecipient"); + spyOn($.fn, "typeahead"); + var person = {name: "diaspora user"}; + this.target.onSuggestionSelection(person); + expect(app.views.ConversationsForm.prototype.addRecipient).toHaveBeenCalledWith(person); + expect($.fn.typeahead).toHaveBeenCalledWith("val", ""); + }); }); describe("keyDown", function() { beforeEach(function() { this.submitCallback = jasmine.createSpy().and.returnValue(false); - new app.views.ConversationsForm(); }); context("on new message form", function() { @@ -52,6 +173,54 @@ describe("app.views.ConversationsForm", function() { }); }); + describe("removeRecipient", function() { + beforeEach(function() { + this.target.addRecipient({id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}); + this.target.addRecipient({id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}); + this.target.addRecipient({id: 3, name: "user@pod.tld", handle: "user@pod.tld"}); + }); + + it("removes the user from conversation recipients when clicking the tag's remove button", function() { + expect(this.target.conversationRecipients).toEqual([ + {id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}, + {id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}, + {id: 3, name: "user@pod.tld", handle: "user@pod.tld"} + ]); + + $("[data-diaspora-handle='diaspora-user@pod.tld'] .remove").click(); + + expect(this.target.conversationRecipients).toEqual([ + {id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}, + {id: 3, name: "user@pod.tld", handle: "user@pod.tld"} + ]); + + $("[data-diaspora-handle='other-diaspora-user@pod.tld'] .remove").click(); + + expect(this.target.conversationRecipients).toEqual([ + {id: 3, name: "user@pod.tld", handle: "user@pod.tld"} + ]); + + $("[data-diaspora-handle='user@pod.tld'] .remove").click(); + + expect(this.target.conversationRecipients).toEqual([]); + }); + + it("removes the tag element when clicking the tag's remove button", function() { + expect($("[data-diaspora-handle='diaspora-user@pod.tld']").length).toBe(1); + $("[data-diaspora-handle='diaspora-user@pod.tld'] .remove").click(); + expect($("[data-diaspora-handle='other-diaspora-user@pod.tld']").length).toBe(1); + $("[data-diaspora-handle='other-diaspora-user@pod.tld'] .remove").click(); + expect($("[data-diaspora-handle='user@pod.tld']").length).toBe(1); + $("[data-diaspora-handle='user@pod.tld'] .remove").click(); + }); + + it("calls updateContactIdsListInput", function() { + spyOn(app.views.ConversationsForm.prototype, "updateContactIdsListInput"); + $("[data-diaspora-handle='diaspora-user@pod.tld'] .remove").click(); + expect(app.views.ConversationsForm.prototype.updateContactIdsListInput).toHaveBeenCalled(); + }); + }); + describe("conversationCreateSuccess", function() { it("is called when there was a successful ajax request for the conversation form", function() { spyOn(app.views.ConversationsForm.prototype, "conversationCreateSuccess"); diff --git a/spec/javascripts/app/views/profile_header_view_spec.js b/spec/javascripts/app/views/profile_header_view_spec.js index 724cc7260..f6e78d934 100644 --- a/spec/javascripts/app/views/profile_header_view_spec.js +++ b/spec/javascripts/app/views/profile_header_view_spec.js @@ -30,4 +30,30 @@ describe("app.views.ProfileHeader", function() { })); }); }); + + describe("showMessageModal", function() { + beforeEach(function() { + $("body").append("
").append(spec.readFixture("conversations_modal")); + }); + + it("calls app.helpers.showModal", function() { + spyOn(app.helpers, "showModal"); + this.view.showMessageModal(); + expect(app.helpers.showModal); + }); + + it("app.views.ConversationsForm with correct parameterswhen modal is loaded", function() { + gon.conversationPrefill = [ + {id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}, + {id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}, + {id: 3, name: "user@pod.tld", handle: "user@pod.tld"} + ]; + + spyOn(app.views.ConversationsForm.prototype, "initialize"); + this.view.showMessageModal(); + $("#conversationModal").trigger("modal:loaded"); + expect(app.views.ConversationsForm.prototype.initialize) + .toHaveBeenCalledWith({prefill: gon.conversationPrefill}); + }); + }); }); diff --git a/spec/javascripts/app/views/publisher_mention_view_spec.js b/spec/javascripts/app/views/publisher_mention_view_spec.js index 436e5e674..d4e9417e3 100644 --- a/spec/javascripts/app/views/publisher_mention_view_spec.js +++ b/spec/javascripts/app/views/publisher_mention_view_spec.js @@ -19,7 +19,7 @@ describe("app.views.PublisherMention", function() { expect(call.args[0].typeaheadInput.selector).toBe("#publisher .typeahead-mention-box"); expect(call.args[0].customSearch).toBeTruthy(); expect(call.args[0].autoselect).toBeTruthy(); - expect(call.args[0].remoteRoute).toBe("/contacts"); + expect(call.args[0].remoteRoute).toEqual({url: "/contacts"}); }); it("calls bindTypeaheadEvents", function() { diff --git a/spec/javascripts/app/views/search_view_spec.js b/spec/javascripts/app/views/search_view_spec.js index 74e4831eb..ffb652304 100644 --- a/spec/javascripts/app/views/search_view_spec.js +++ b/spec/javascripts/app/views/search_view_spec.js @@ -11,7 +11,7 @@ describe("app.views.Search", function() { this.view = new app.views.Search({el: "#search_people_form"}); var call = app.views.SearchBase.prototype.initialize.calls.mostRecent(); expect(call.args[0].typeaheadInput.selector).toBe("#search_people_form #q"); - expect(call.args[0].remoteRoute).toBe("/search"); + expect(call.args[0].remoteRoute).toEqual({url: "/search"}); }); it("binds typeahead:select", function() { From 2b6465ef258c252ec2e7b34a71a62b4533c9fe0a Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Sun, 13 Nov 2016 15:10:15 +0100 Subject: [PATCH 20/71] Use !== instead of localeCompare when removing conversation recipients --- app/assets/javascripts/app/views/conversations_form_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/app/views/conversations_form_view.js b/app/assets/javascripts/app/views/conversations_form_view.js index 0b304739b..74f848de3 100644 --- a/app/assets/javascripts/app/views/conversations_form_view.js +++ b/app/assets/javascripts/app/views/conversations_form_view.js @@ -75,7 +75,7 @@ app.views.ConversationsForm = Backbone.View.extend({ var diasporaHandle = $recipientTagEl.data("diaspora-handle"); this.conversationRecipients = this.conversationRecipients.filter(function(person) { - return diasporaHandle.localeCompare(person.handle) !== 0; + return diasporaHandle !== person.handle; }); this.updateContactIdsListInput(); From 9cbadec65966a04b6445fff9c8a6c803ea533754 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Sun, 13 Nov 2016 15:22:03 +0100 Subject: [PATCH 21/71] Move 'XSS via name parameter' to mobile conversation specs because the desktop view doesn't use the name parameter anymore --- .../conversations_controller_spec.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/controllers/conversations_controller_spec.rb b/spec/controllers/conversations_controller_spec.rb index 86709b263..1c824128b 100644 --- a/spec/controllers/conversations_controller_spec.rb +++ b/spec/controllers/conversations_controller_spec.rb @@ -17,7 +17,7 @@ describe ConversationsController, :type => :controller do end describe "#new modal" do - context "desktop and mobile" do + context "desktop" do it "succeeds" do get :new, modal: true expect(response).to be_success @@ -32,14 +32,6 @@ describe ConversationsController, :type => :controller do get :new, aspect_id: alice.aspects.first.id, modal: true expect(controller.gon.conversation_prefill).to eq(alice.aspects.first.contacts.map {|c| c.person.as_json }) end - - it "does not allow XSS via the name parameter" do - ["", - '"}]});alert(1);(function f() {var foo = [{b:"'].each do |xss| - get :new, modal: true, name: xss - expect(response.body).not_to include xss - end - end end context "mobile" do @@ -58,6 +50,14 @@ describe ConversationsController, :type => :controller do expect(assigns(:contacts_json)).not_to include(alice.contacts.where(receiving: false).first.person.name) end + it "does not allow XSS via the name parameter" do + ["", + '"}]});alert(1);(function f() {var foo = [{b:"'].each do |xss| + get :new, modal: true, name: xss + expect(response.body).not_to include xss + end + end + it "does not allow XSS via the profile name" do xss = "" contact = alice.contacts.first From 4c86b645327298a57206fd367e029a813b8760a5 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Sun, 13 Nov 2016 15:49:11 +0100 Subject: [PATCH 22/71] Improve contacts page jasmine test * append html to spec.content() instead of body * don't append the unused conversations modal fixture * actually test that showModal has been called --- spec/javascripts/app/pages/contacts_spec.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/javascripts/app/pages/contacts_spec.js b/spec/javascripts/app/pages/contacts_spec.js index 8574e6a5f..a1be4be48 100644 --- a/spec/javascripts/app/pages/contacts_spec.js +++ b/spec/javascripts/app/pages/contacts_spec.js @@ -280,16 +280,16 @@ describe("app.pages.Contacts", function(){ describe("showMessageModal", function() { beforeEach(function() { - $("body").append("
").append(spec.readFixture("conversations_modal")); + spec.content().append("
"); }); it("calls app.helpers.showModal", function() { spyOn(app.helpers, "showModal"); this.view.showMessageModal(); - expect(app.helpers.showModal); + expect(app.helpers.showModal).toHaveBeenCalled(); }); - it("app.views.ConversationsForm with correct parameters when modal is loaded", function() { + it("initializes app.views.ConversationsForm with correct parameters when modal is loaded", function() { gon.conversationPrefill = [ {id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}, {id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}, @@ -299,7 +299,6 @@ describe("app.pages.Contacts", function(){ spyOn(app.views.ConversationsForm.prototype, "initialize"); this.view.showMessageModal(); $("#conversationModal").trigger("modal:loaded"); - expect($("#conversationModal").length).toBe(1); expect(app.views.ConversationsForm.prototype.initialize) .toHaveBeenCalledWith({prefill: gon.conversationPrefill}); }); From cbe3ca5cf62c0f137c0560fde268e985b83731e6 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Sun, 13 Nov 2016 16:02:27 +0100 Subject: [PATCH 23/71] Improve conversations form jasmine test * test that tag element are removed when clicking the tag's remove button --- .../app/views/conversations_form_view_spec.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/spec/javascripts/app/views/conversations_form_view_spec.js b/spec/javascripts/app/views/conversations_form_view_spec.js index 97760a067..46c170f3a 100644 --- a/spec/javascripts/app/views/conversations_form_view_spec.js +++ b/spec/javascripts/app/views/conversations_form_view_spec.js @@ -43,13 +43,13 @@ describe("app.views.ConversationsForm", function() { $("#conversation-show").addClass("hidden"); }); - it("add the participant", function() { + it("adds the participant", function() { expect(this.target.conversationRecipients).toEqual([]); this.target.addRecipient({name: "diaspora user", handle: "diaspora-user@pod.tld"}); expect(this.target.conversationRecipients).toEqual([{name: "diaspora user", handle: "diaspora-user@pod.tld"}]); }); - it("call updateContactIdsListInput", function() { + it("calls updateContactIdsListInput", function() { spyOn(app.views.ConversationsForm.prototype, "updateContactIdsListInput"); this.target.addRecipient({name: "diaspora user", handle: "diaspora-user@pod.tld"}); expect(app.views.ConversationsForm.prototype.updateContactIdsListInput).toHaveBeenCalled(); @@ -67,7 +67,7 @@ describe("app.views.ConversationsForm", function() { this.prefills = [{name: "diaspora user"}, {name: "other diaspora user"}, {name: "user"}]; }); - it("call addRecipient for each prefilled participant", function() { + it("calls addRecipient for each prefilled participant", function() { spyOn(app.views.ConversationsForm.prototype, "addRecipient"); this.target.prefill(this.prefills); expect(app.views.ConversationsForm.prototype.addRecipient).toHaveBeenCalledTimes(this.prefills.length); @@ -115,7 +115,7 @@ describe("app.views.ConversationsForm", function() { }); describe("onSuggestionSelection", function() { - it("calls addRecipient, updateContactIdsListInput and $.fn.typeahead", function() { + it("calls addRecipient and $.fn.typeahead", function() { spyOn(app.views.ConversationsForm.prototype, "addRecipient"); spyOn($.fn, "typeahead"); var person = {name: "diaspora user"}; @@ -208,10 +208,15 @@ describe("app.views.ConversationsForm", function() { it("removes the tag element when clicking the tag's remove button", function() { expect($("[data-diaspora-handle='diaspora-user@pod.tld']").length).toBe(1); $("[data-diaspora-handle='diaspora-user@pod.tld'] .remove").click(); + expect($("[data-diaspora-handle='diaspora-user@pod.tld']").length).toBe(0); + expect($("[data-diaspora-handle='other-diaspora-user@pod.tld']").length).toBe(1); $("[data-diaspora-handle='other-diaspora-user@pod.tld'] .remove").click(); + expect($("[data-diaspora-handle='other-diaspora-user@pod.tld']").length).toBe(0); + expect($("[data-diaspora-handle='user@pod.tld']").length).toBe(1); $("[data-diaspora-handle='user@pod.tld'] .remove").click(); + expect($("[data-diaspora-handle='user@pod.tld']").length).toBe(0); }); it("calls updateContactIdsListInput", function() { From 8983554f949a1664eeeff5493bf10e09ce5b14ba Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Sun, 13 Nov 2016 16:06:07 +0100 Subject: [PATCH 24/71] Improve profile header jasmine test * append html to spec.content() instead of body * don't append the unused conversations modal fixture * actually test that showModal has been called --- spec/javascripts/app/views/profile_header_view_spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/javascripts/app/views/profile_header_view_spec.js b/spec/javascripts/app/views/profile_header_view_spec.js index f6e78d934..17bfecc81 100644 --- a/spec/javascripts/app/views/profile_header_view_spec.js +++ b/spec/javascripts/app/views/profile_header_view_spec.js @@ -33,16 +33,16 @@ describe("app.views.ProfileHeader", function() { describe("showMessageModal", function() { beforeEach(function() { - $("body").append("
").append(spec.readFixture("conversations_modal")); + spec.content().append("
"); }); it("calls app.helpers.showModal", function() { spyOn(app.helpers, "showModal"); this.view.showMessageModal(); - expect(app.helpers.showModal); + expect(app.helpers.showModal).toHaveBeenCalled(); }); - it("app.views.ConversationsForm with correct parameterswhen modal is loaded", function() { + it("initializes app.views.ConversationsForm with correct parameters when modal is loaded", function() { gon.conversationPrefill = [ {id: 1, name: "diaspora user", handle: "diaspora-user@pod.tld"}, {id: 2, name: "other diaspora user", handle: "other-diaspora-user@pod.tld"}, From 117b17e25eb784657e9d2f3179ad770304545be9 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Sun, 13 Nov 2016 16:08:19 +0100 Subject: [PATCH 25/71] Remove unused conversations modal fixture closes #7129 --- Changelog.md | 1 + spec/controllers/jasmine_fixtures/conversations_spec.rb | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Changelog.md b/Changelog.md index 229fc8d47..5219d1865 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,7 @@ * Show spinner when loading comments in the stream [#7170](https://github.com/diaspora/diaspora/pull/7170) * Add a dark color theme [#7152](https://github.com/diaspora/diaspora/pull/7152) * Added setting for custom changelog URL [#7166](https://github.com/diaspora/diaspora/pull/7166) +* Show more information of recipients on conversation creation [#7129](https://github.com/diaspora/diaspora/pull/7129) # 0.6.1.0 diff --git a/spec/controllers/jasmine_fixtures/conversations_spec.rb b/spec/controllers/jasmine_fixtures/conversations_spec.rb index ac74bb7f8..c07360d69 100644 --- a/spec/controllers/jasmine_fixtures/conversations_spec.rb +++ b/spec/controllers/jasmine_fixtures/conversations_spec.rb @@ -29,9 +29,6 @@ describe ConversationsController, :type => :controller do get :index, :conversation_id => @conv1.id save_fixture(html_for("body"), "conversations_read") - - get :new, modal: true - save_fixture(response.body, "conversations_modal") end end From 7cd2232812fe3b4caa9a598192a0154597398168 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Sun, 13 Nov 2016 23:06:35 +0100 Subject: [PATCH 26/71] Only allow conversation creation in controller with mututal contacts --- app/controllers/conversations_controller.rb | 2 +- .../conversations_controller_spec.rb | 69 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/app/controllers/conversations_controller.rb b/app/controllers/conversations_controller.rb index 4512e8a3c..b667e31e4 100644 --- a/app/controllers/conversations_controller.rb +++ b/app/controllers/conversations_controller.rb @@ -35,7 +35,7 @@ class ConversationsController < ApplicationController # This will have to be removed when mobile autocomplete is ported to Typeahead recipients_param, column = [%i(contact_ids id), %i(person_ids person_id)].find {|param, _| params[param].present? } if recipients_param - person_ids = current_user.contacts.where(column => params[recipients_param].split(",")).pluck(:person_id) + person_ids = current_user.contacts.mutual.where(column => params[recipients_param].split(",")).pluck(:person_id) end opts = params.require(:conversation).permit(:subject) diff --git a/spec/controllers/conversations_controller_spec.rb b/spec/controllers/conversations_controller_spec.rb index 1c824128b..f3a0249e7 100644 --- a/spec/controllers/conversations_controller_spec.rb +++ b/spec/controllers/conversations_controller_spec.rb @@ -272,6 +272,39 @@ describe ConversationsController, :type => :controller do expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) end end + + context "with non-mutual contact" do + before do + @person1 = FactoryGirl.create(:person) + @person2 = FactoryGirl.create(:person) + alice.contacts.create!(receiving: false, sharing: true, person: @person2) + @person3 = FactoryGirl.create(:person) + alice.contacts.create!(receiving: true, sharing: false, person: @person3) + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + person_ids: [@person1.id, @person2.id, @person3.id] + } + end + + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end + + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end + + it "responds with an error message" do + post :create, @hash + expect(response).not_to be_success + expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) + end + end end context "mobile" do @@ -418,6 +451,42 @@ describe ConversationsController, :type => :controller do post :create, @hash expect(Message.count).to eq(count) end + + it "responds with an error message" do + post :create, @hash + expect(response).not_to be_success + expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) + end + end + + context "with non-mutual contact" do + before do + @contact1 = alice.contacts.create(receiving: false, sharing: true, person: FactoryGirl.create(:person)) + @contact2 = alice.contacts.create(receiving: true, sharing: false, person: FactoryGirl.create(:person)) + @hash = { + format: :js, + conversation: {subject: "secret stuff", text: "text debug"}, + person_ids: [@contact1.id, @contact2.id] + } + end + + it "does not create a conversation" do + count = Conversation.count + post :create, @hash + expect(Conversation.count).to eq(count) + end + + it "does not create a message" do + count = Message.count + post :create, @hash + expect(Message.count).to eq(count) + end + + it "responds with an error message" do + post :create, @hash + expect(response).not_to be_success + expect(response.body).to eq(I18n.t("javascripts.conversation.create.no_recipient")) + end end end end From 457cf2aafb89a778821856350b0e049a3891145a Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Sun, 13 Nov 2016 23:10:47 +0100 Subject: [PATCH 27/71] Fix deprecation warnings in conversations controller spec closes #7187 --- .../controllers/conversations_controller_spec.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/controllers/conversations_controller_spec.rb b/spec/controllers/conversations_controller_spec.rb index f3a0249e7..d02ed22fe 100644 --- a/spec/controllers/conversations_controller_spec.rb +++ b/spec/controllers/conversations_controller_spec.rb @@ -131,7 +131,7 @@ describe ConversationsController, :type => :controller do @hash = { format: :js, conversation: {subject: "secret stuff", text: "text debug"}, - person_ids: [alice.contacts.first.person.id] + person_ids: alice.contacts.first.person.id.to_s } end @@ -170,7 +170,7 @@ describe ConversationsController, :type => :controller do @hash = { format: :js, conversation: {subject: " ", text: "text debug"}, - person_ids: [alice.contacts.first.person.id] + person_ids: alice.contacts.first.person.id.to_s } end @@ -194,7 +194,7 @@ describe ConversationsController, :type => :controller do @hash = { format: :js, conversation: {subject: "secret stuff", text: " "}, - person_ids: [alice.contacts.first.person.id] + person_ids: alice.contacts.first.person.id.to_s } end @@ -283,7 +283,7 @@ describe ConversationsController, :type => :controller do @hash = { format: :js, conversation: {subject: "secret stuff", text: "text debug"}, - person_ids: [@person1.id, @person2.id, @person3.id] + person_ids: [@person1.id, @person2.id, @person3.id].join(",") } end @@ -317,7 +317,7 @@ describe ConversationsController, :type => :controller do @hash = { format: :js, conversation: {subject: "secret stuff", text: "text debug"}, - contact_ids: [alice.contacts.first.id] + contact_ids: alice.contacts.first.id.to_s } end @@ -356,7 +356,7 @@ describe ConversationsController, :type => :controller do @hash = { format: :js, conversation: {subject: " ", text: "text debug"}, - contact_ids: [alice.contacts.first.id] + contact_ids: alice.contacts.first.id.to_s } end @@ -380,7 +380,7 @@ describe ConversationsController, :type => :controller do @hash = { format: :js, conversation: {subject: "secret stuff", text: " "}, - contact_ids: [alice.contacts.first.id] + contact_ids: alice.contacts.first.id.to_s } end @@ -466,7 +466,7 @@ describe ConversationsController, :type => :controller do @hash = { format: :js, conversation: {subject: "secret stuff", text: "text debug"}, - person_ids: [@contact1.id, @contact2.id] + person_ids: [@contact1.id, @contact2.id].join(",") } end From 1863137161303e9460883daa3a4bb66d515e16ed Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Fri, 11 Nov 2016 12:13:29 +0100 Subject: [PATCH 28/71] Use string-direction gem for rtl detection closes #7181 --- Changelog.md | 1 + Gemfile | 4 ++++ Gemfile.lock | 5 +++- lib/direction_detector.rb | 34 ++------------------------- spec/lib/direction_detector_spec.rb | 36 +++++------------------------ 5 files changed, 17 insertions(+), 63 deletions(-) diff --git a/Changelog.md b/Changelog.md index 5219d1865..3328d4968 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,7 @@ # 0.6.2.0 ## Refactor +* Use string-direction gem for rtl detection [#7181](https://github.com/diaspora/diaspora/pull/7181) ## Bug fixes * Fix fetching comments after fetching likes [#7167](https://github.com/diaspora/diaspora/pull/7167) diff --git a/Gemfile b/Gemfile index e4614cfd7..9bf0486c6 100644 --- a/Gemfile +++ b/Gemfile @@ -135,6 +135,10 @@ gem "twitter-text", "1.14.0" gem "ruby-oembed", "0.10.1" gem "open_graph_reader", "0.6.1" +# RTL support + +gem "string-direction", "1.2.0" + # Security Headers gem "secure_headers", "3.5.0" diff --git a/Gemfile.lock b/Gemfile.lock index 73f20f63e..73559ce03 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -819,6 +819,8 @@ GEM activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) state_machine (1.2.0) + string-direction (1.2.0) + yard (~> 0.8) swd (1.0.1) activesupport (>= 3) attr_required (>= 0.0.5) @@ -1031,6 +1033,7 @@ DEPENDENCIES spring (= 2.0.0) spring-commands-cucumber (= 1.0.1) spring-commands-rspec (= 1.0.4) + string-direction (= 1.2.0) test_after_commit (= 1.1.0) timecop (= 0.8.1) turbo_dev_assets (= 0.0.2) @@ -1046,4 +1049,4 @@ DEPENDENCIES will_paginate (= 3.1.5) BUNDLED WITH - 1.13.5 + 1.13.6 diff --git a/lib/direction_detector.rb b/lib/direction_detector.rb index 9724c80c8..c7ffd055a 100644 --- a/lib/direction_detector.rb +++ b/lib/direction_detector.rb @@ -2,36 +2,16 @@ # Copyright (c) 2010-2011, Diaspora Inc. This file is # licensed under the Affero General Public License version 3 or later. See # the COPYRIGHT file. -# Deeply inspired by https://gitorious.org/statusnet/mainline/blobs/master/plugins/DirectionDetector/DirectionDetectorPlugin.php class String - RTL_RANGES = [ - [1536, 1791], # arabic, persian, urdu, kurdish, ... - [65136, 65279], # arabic peresent 2 - [64336, 65023], # arabic peresent 1 - [1424, 1535], # hebrew - [64256, 64335], # hebrew peresent - [1792, 1871], # syriac - [1920, 1983], # thaana - [1984, 2047], # nko - [11568, 11647] # tifinagh - ] RTL_CLEANER_REGEXES = [ /@[^ ]+|#[^ ]+/u, # mention, tag /^RT[: ]{1}| RT | RT: |[♺♻:]/u # retweet ] def is_rtl? return false if self.strip.empty? - count = 0 - self.split(" ").each do |word| - if starts_with_rtl_char?(word) - count += 1 - else - count -= 1 - end - end - return true if count > 0 # more than half of the words are rtl words - return starts_with_rtl_char?(self) # otherwise let the first word decide + detector = StringDirection::Detector.new(:dominant) + detector.rtl? self end # Diaspora specific @@ -42,14 +22,4 @@ class String end string.is_rtl? end - - def starts_with_rtl_char?(string = self) - stripped = string.strip - return false if stripped.empty? - char = stripped.unpack('U*').first - RTL_RANGES.each do |limit| - return true if char >= limit[0] && char <= limit[1] - end - return false - end end diff --git a/spec/lib/direction_detector_spec.rb b/spec/lib/direction_detector_spec.rb index b19e854db..9b28700e3 100644 --- a/spec/lib/direction_detector_spec.rb +++ b/spec/lib/direction_detector_spec.rb @@ -24,29 +24,6 @@ describe String do let(:hebrew_arabic) { "#{hebrew} #{arabic}" } - describe "#stats_with_rtl_char?" do - it 'returns true or false correctly' do - expect(english.starts_with_rtl_char?).to be false - expect(chinese.starts_with_rtl_char?).to be false - expect(arabic.starts_with_rtl_char?).to be true - expect(hebrew.starts_with_rtl_char?).to be true - expect(hebrew_arabic.starts_with_rtl_char?).to be true - end - - it 'only looks at the first char' do - expect(english_chinese.starts_with_rtl_char?).to be false - expect(chinese_english.starts_with_rtl_char?).to be false - expect(english_arabic.starts_with_rtl_char?).to be false - expect(hebrew_english.starts_with_rtl_char?).to be true - expect(arabic_chinese.starts_with_rtl_char?).to be true - end - - it 'ignores whitespaces' do - expect(" \n \r \t".starts_with_rtl_char?).to be false - expect(" #{arabic} ".starts_with_rtl_char?).to be true - end - end - describe "#is_rtl?" do it 'returns true or false correctly' do expect(english.is_rtl?).to be false @@ -65,17 +42,16 @@ describe String do expect("#{english} #{arabic} #{arabic}".is_rtl?).to be true end - it "fallbacks to the first word if there's no majority" do - expect(hebrew_english.is_rtl?).to be true - expect(english_hebrew.is_rtl?).to be false - expect(arabic_english.is_rtl?).to be true - expect(english_arabic.is_rtl?).to be false - end - it 'ignores whitespaces' do expect(" \n \r \t".is_rtl?).to be false expect(" #{arabic} ".is_rtl?).to be true end + + it "ignores byte order marks" do + expect("\u{feff}".is_rtl?).to be false + expect("\u{feff}#{arabic}".is_rtl?).to be true + expect("\u{feff}#{english}".is_rtl?).to be false + end end describe '#cleaned_is_rtl?' do From 3930069e675253e3237b491cb8cb7be9751f91e1 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Sat, 12 Nov 2016 21:56:56 +0100 Subject: [PATCH 29/71] Leave help view spec with the default locale --- spec/javascripts/app/views/help_view_spec.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/javascripts/app/views/help_view_spec.js b/spec/javascripts/app/views/help_view_spec.js index 098e7dfcc..38717a620 100644 --- a/spec/javascripts/app/views/help_view_spec.js +++ b/spec/javascripts/app/views/help_view_spec.js @@ -2,11 +2,17 @@ describe("app.views.Help", function(){ beforeEach(function(){ gon.appConfig = {chat: {enabled: false}}; this.locale = JSON.parse(spec.readFixture("locale_en_help_json")); + Diaspora.I18n.reset(); Diaspora.I18n.load(this.locale, "en"); this.view = new app.views.Help(); Diaspora.Page = "HelpFaq"; }); + afterEach(function() { + Diaspora.I18n.reset(); + Diaspora.I18n.load(spec.defaultLocale); + }); + describe("render", function(){ beforeEach(function(){ this.view.render(); From a951c40ba0bb8fd2cae94959af607cc80290fa8a Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Sat, 12 Nov 2016 21:31:25 +0100 Subject: [PATCH 30/71] Only reload profile header when changing aspect memberships closes #7183 fixes #7072 --- Changelog.md | 1 + app/assets/javascripts/app/pages/profile.js | 1 - .../app/views/profile_header_view.js | 1 + .../app/views/profile_header_view_spec.js | 28 +++++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 3328d4968..e35adc2cc 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ ## Bug fixes * Fix fetching comments after fetching likes [#7167](https://github.com/diaspora/diaspora/pull/7167) * Hide 'reshare' button on already reshared posts [#7169](https://github.com/diaspora/diaspora/pull/7169) +* Only reload profile header when changing aspect memberships [#7183](https://github.com/diaspora/diaspora/pull/7183) ## Features * Show spinner when loading comments in the stream [#7170](https://github.com/diaspora/diaspora/pull/7170) diff --git a/app/assets/javascripts/app/pages/profile.js b/app/assets/javascripts/app/pages/profile.js index b987ea387..a2b814399 100644 --- a/app/assets/javascripts/app/pages/profile.js +++ b/app/assets/javascripts/app/pages/profile.js @@ -31,7 +31,6 @@ app.pages.Profile = app.views.Base.extend({ this.streamCollection = _.has(opts, "streamCollection") ? opts.streamCollection : null; this.streamViewClass = _.has(opts, "streamView") ? opts.streamView : null; - this.model.on("change", this.render, this); this.model.on("sync", this._done, this); // bind to global events diff --git a/app/assets/javascripts/app/views/profile_header_view.js b/app/assets/javascripts/app/views/profile_header_view.js index 0fc671b8e..1522b71e4 100644 --- a/app/assets/javascripts/app/views/profile_header_view.js +++ b/app/assets/javascripts/app/views/profile_header_view.js @@ -15,6 +15,7 @@ app.views.ProfileHeader = app.views.Base.extend({ initialize: function(opts) { this.photos = _.has(opts, 'photos') ? opts.photos : null; this.contacts = _.has(opts, 'contacts') ? opts.contacts : null; + this.model.on("change", this.render, this); $("#mentionModal").on("modal:loaded", this.mentionModalLoaded.bind(this)); $("#mentionModal").on("hidden.bs.modal", this.mentionModalHidden); }, diff --git a/spec/javascripts/app/views/profile_header_view_spec.js b/spec/javascripts/app/views/profile_header_view_spec.js index 17bfecc81..ebd4ecde1 100644 --- a/spec/javascripts/app/views/profile_header_view_spec.js +++ b/spec/javascripts/app/views/profile_header_view_spec.js @@ -11,6 +11,34 @@ describe("app.views.ProfileHeader", function() { loginAs(factory.userAttrs()); }); + describe("initialize", function() { + it("calls #render when the model changes", function() { + spyOn(app.views.ProfileHeader.prototype, "render"); + this.view.initialize(); + expect(app.views.ProfileHeader.prototype.render).not.toHaveBeenCalled(); + this.view.model.trigger("change"); + expect(app.views.ProfileHeader.prototype.render).toHaveBeenCalled(); + }); + + it("calls #mentionModalLoaded on modal:loaded", function() { + spec.content().append("
"); + spyOn(app.views.ProfileHeader.prototype, "mentionModalLoaded"); + this.view.initialize(); + expect(app.views.ProfileHeader.prototype.mentionModalLoaded).not.toHaveBeenCalled(); + $("#mentionModal").trigger("modal:loaded"); + expect(app.views.ProfileHeader.prototype.mentionModalLoaded).toHaveBeenCalled(); + }); + + it("calls #mentionModalHidden on hidden.bs.modal", function() { + spec.content().append("
"); + spyOn(app.views.ProfileHeader.prototype, "mentionModalHidden"); + this.view.initialize(); + expect(app.views.ProfileHeader.prototype.mentionModalHidden).not.toHaveBeenCalled(); + $("#mentionModal").trigger("hidden.bs.modal"); + expect(app.views.ProfileHeader.prototype.mentionModalHidden).toHaveBeenCalled(); + }); + }); + context("#presenter", function() { it("contains necessary elements", function() { expect(this.view.presenter()).toEqual(jasmine.objectContaining({ From cdce25374ff1464296aa3e91d69740d51a9ba99e Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Sat, 12 Nov 2016 22:49:07 +0100 Subject: [PATCH 31/71] Reduce i18n.load side effects closes #7184 --- Changelog.md | 1 + app/assets/javascripts/helpers/i18n.js | 2 +- spec/javascripts/helpers/i18n_spec.js | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index e35adc2cc..d5df0dfd4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,7 @@ ## Refactor * Use string-direction gem for rtl detection [#7181](https://github.com/diaspora/diaspora/pull/7181) +* Reduce i18n.load side effects [#7184](https://github.com/diaspora/diaspora/pull/7184) ## Bug fixes * Fix fetching comments after fetching likes [#7167](https://github.com/diaspora/diaspora/pull/7167) diff --git a/app/assets/javascripts/helpers/i18n.js b/app/assets/javascripts/helpers/i18n.js index 42bd7a886..e6db0f2e1 100644 --- a/app/assets/javascripts/helpers/i18n.js +++ b/app/assets/javascripts/helpers/i18n.js @@ -18,7 +18,7 @@ Diaspora.I18n = { }, updateLocale: function(locale, data) { - locale.data = $.extend(locale.data, data); + locale.data = $.extend({}, locale.data, data); var rule = locale.data.pluralization_rule; if (typeof rule !== "undefined") { diff --git a/spec/javascripts/helpers/i18n_spec.js b/spec/javascripts/helpers/i18n_spec.js index 79dfabe27..0190a454f 100644 --- a/spec/javascripts/helpers/i18n_spec.js +++ b/spec/javascripts/helpers/i18n_spec.js @@ -45,6 +45,28 @@ describe("Diaspora.I18n", function() { expect(Diaspora.I18n.locale.data).toEqual(extended); }); + + it("overrides existing translations", function() { + var oldLocale = {name: "Bob"}; + var newLocale = {name: "Alice"}; + Diaspora.I18n.load(oldLocale, "en"); + expect(Diaspora.I18n.locale.data.name).toBe("Bob"); + Diaspora.I18n.load(newLocale, "en"); + expect(Diaspora.I18n.locale.data.name).toBe("Alice"); + + Diaspora.I18n.reset(oldLocale); + expect(Diaspora.I18n.locale.data.name).toBe("Bob"); + Diaspora.I18n.load(newLocale, "en"); + expect(Diaspora.I18n.locale.data.name).toBe("Alice"); + }); + + it("doesn't change locale objects given in ealier calls", function() { + var oldLocale = {name: "Bob"}; + var newLocale = {name: "Alice"}; + Diaspora.I18n.reset(oldLocale); + Diaspora.I18n.load(newLocale, "en"); + expect(oldLocale.name).toBe("Bob"); + }); }); describe("::t", function() { From 37b34237b9909f5346d9d1ef7cd0a5d2df4c78fc Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Sat, 12 Nov 2016 23:30:03 +0100 Subject: [PATCH 32/71] Force jasmine fails on syntax errors closes #7185 --- Changelog.md | 1 + spec/javascripts/onerror-fail.js | 7 +++++++ spec/javascripts/support/jasmine.yml | 1 + 3 files changed, 9 insertions(+) create mode 100644 spec/javascripts/onerror-fail.js diff --git a/Changelog.md b/Changelog.md index d5df0dfd4..f4f8d6e42 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,6 +3,7 @@ ## Refactor * Use string-direction gem for rtl detection [#7181](https://github.com/diaspora/diaspora/pull/7181) * Reduce i18n.load side effects [#7184](https://github.com/diaspora/diaspora/pull/7184) +* Force jasmine fails on syntax errors [#7185](https://github.com/diaspora/diaspora/pull/7185) ## Bug fixes * Fix fetching comments after fetching likes [#7167](https://github.com/diaspora/diaspora/pull/7167) diff --git a/spec/javascripts/onerror-fail.js b/spec/javascripts/onerror-fail.js new file mode 100644 index 000000000..21f69b745 --- /dev/null +++ b/spec/javascripts/onerror-fail.js @@ -0,0 +1,7 @@ +window.onerror = function(errorMsg, url, lineNumber) { + describe("Test suite", function() { + it("shouldn't skip tests because of syntax errors", function() { + fail(errorMsg + " in file " + url + " in line " + lineNumber); + }); + }); +}; diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml index 87e516cca..cec433307 100644 --- a/spec/javascripts/support/jasmine.yml +++ b/spec/javascripts/support/jasmine.yml @@ -53,6 +53,7 @@ helpers: # - **/*[sS]pec.js # spec_files: + - onerror-fail.js - "**/**/*[sS]pec.js" # src_dir From c7a0b053fa0382649672564ca25b65ac3f8d2404 Mon Sep 17 00:00:00 2001 From: Justin Ramos Date: Thu, 17 Nov 2016 13:19:11 +0000 Subject: [PATCH 33/71] fixing invitations modal visibility issue closes #7191 --- Changelog.md | 1 + app/views/shared/_invitations.haml | 9 +++++---- features/desktop/invitations.feature | 11 ++++++++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Changelog.md b/Changelog.md index f4f8d6e42..b7fd34534 100644 --- a/Changelog.md +++ b/Changelog.md @@ -9,6 +9,7 @@ * Fix fetching comments after fetching likes [#7167](https://github.com/diaspora/diaspora/pull/7167) * Hide 'reshare' button on already reshared posts [#7169](https://github.com/diaspora/diaspora/pull/7169) * Only reload profile header when changing aspect memberships [#7183](https://github.com/diaspora/diaspora/pull/7183) +* Fix visiblity on invitation modal when opening it from the stream [#7191](https://github.com/diaspora/diaspora/pull/7191) ## Features * Show spinner when loading comments in the stream [#7170](https://github.com/diaspora/diaspora/pull/7170) diff --git a/app/views/shared/_invitations.haml b/app/views/shared/_invitations.haml index 4c45deee7..2ddd9e6d9 100644 --- a/app/views/shared/_invitations.haml +++ b/app/views/shared/_invitations.haml @@ -3,7 +3,8 @@ .invitations-link.btn.btn-link#invitations-button{"data-toggle" => "modal"} = t(".by_email") -= render "shared/modal", - path: new_user_invitation_path, - id: "invitationsModal", - title: t("invitations.new.invite_someone_to_join") +- content_for :after_content do + = render "shared/modal", + path: new_user_invitation_path, + id: "invitationsModal", + title: t("invitations.new.invite_someone_to_join") diff --git a/features/desktop/invitations.feature b/features/desktop/invitations.feature index 5aafd11df..0e9e09a61 100644 --- a/features/desktop/invitations.feature +++ b/features/desktop/invitations.feature @@ -39,7 +39,7 @@ Feature: Invitations And I click on selector "#invitations-button" Then I should see one less invite - Scenario: sends an invitation + Scenario: sends an invitation from the sidebar When I sign in as "alice@alice.alice" And I click on "Invite your friends" navbar title And I click on selector "#invitations-button" @@ -49,6 +49,15 @@ Feature: Invitations Then I should have 1 Devise email delivery And I should not see "change your notification settings" in the last sent email + Scenario: sends an invitation from the stream + When I sign in as "alice@alice.alice" + And I press the first "a.invitations-link" within "#no_contacts" + And I fill in the following: + | email_inviter_emails | alex@example.com | + And I press "Send an invitation" + Then I should have 1 Devise email delivery + And I should not see "change your notification settings" in the last sent email + Scenario: sends an invitation from the people search page When I sign in as "alice@alice.alice" And I search for "test" From 00ed1c9b394a458bc08f35311d4ece26139e7e56 Mon Sep 17 00:00:00 2001 From: Justin Ramos Date: Wed, 16 Nov 2016 21:16:00 +0000 Subject: [PATCH 34/71] check AppConfig.mail.enable? before displaying some view content closes #7190 --- Changelog.md | 1 + app/helpers/sessions_helper.rb | 2 +- app/views/devise/shared/_links.haml | 4 +- app/views/invitations/new.html.haml | 45 +++++++-------- app/views/shared/_invitations.haml | 6 +- app/views/users/_edit.haml | 85 +++++++++++++++-------------- 6 files changed, 74 insertions(+), 69 deletions(-) diff --git a/Changelog.md b/Changelog.md index b7fd34534..f17dcabaa 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,7 @@ * Use string-direction gem for rtl detection [#7181](https://github.com/diaspora/diaspora/pull/7181) * Reduce i18n.load side effects [#7184](https://github.com/diaspora/diaspora/pull/7184) * Force jasmine fails on syntax errors [#7185](https://github.com/diaspora/diaspora/pull/7185) +* Don't display mail-related view content if it is disabled in the pod's config [#7190](https://github.com/diaspora/diaspora/pull/7190) ## Bug fixes * Fix fetching comments after fetching likes [#7167](https://github.com/diaspora/diaspora/pull/7167) diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb index 65a9bdaf4..716b12fed 100644 --- a/app/helpers/sessions_helper.rb +++ b/app/helpers/sessions_helper.rb @@ -13,7 +13,7 @@ module SessionsHelper end def display_password_reset_link? - devise_mapping.recoverable? && controller_name != "passwords" + AppConfig.mail.enable? && devise_mapping.recoverable? && controller_name != "passwords" end def flash_class(name) diff --git a/app/views/devise/shared/_links.haml b/app/views/devise/shared/_links.haml index 4c37c1ed5..7f3649484 100644 --- a/app/views/devise/shared/_links.haml +++ b/app/views/devise/shared/_links.haml @@ -1,13 +1,13 @@ - if controller_name != 'sessions' = link_to t('.sign_in'), new_session_path(resource_name) %br/ -- if AppConfig.settings.enable_registrations? && devise_mapping.registerable? && controller_name != 'registrations' +- if display_registration_link? = link_to t('.sign_up'), new_registration_path(resource_name) %br/ - else %b= t('.sign_up_closed') %br/ -- if devise_mapping.recoverable? && controller_name != 'passwords' +- if display_password_reset_link? = link_to t('.forgot_your_password'), new_password_path(resource_name) %br/ - if devise_mapping.confirmable? && controller_name != 'confirmations' diff --git a/app/views/invitations/new.html.haml b/app/views/invitations/new.html.haml index 187a6fa12..8952aead9 100644 --- a/app/views/invitations/new.html.haml +++ b/app/views/invitations/new.html.haml @@ -1,32 +1,33 @@ #paste_link - = t('.paste_link') + = t(".paste_link") %span#codes_left = "(" + t(".codes_left", count: @invite_code.count) + ")" unless AppConfig.settings.enable_registrations? .form-horizontal .control-group = invite_link(@invite_code) -#email_invitation - = form_tag new_user_invitation_path, class: 'form-horizontal' do +- if AppConfig.mail.enable? + #email_invitation + = form_tag new_user_invitation_path, class: "form-horizontal" do - .form-group - %label.col-sm-2.control-label{ for: 'email_inviter_emails' } - = t('email') - .col-sm-10 - = text_field_tag 'email_inviter[emails]', @invalid_emails, title: t('.comma_separated_plz'), - placeholder: 'foo@bar.com, max@foo.com...', class: "form-control" - #already_sent - = t("invitations.create.note_already_sent", emails: @valid_emails) unless @valid_emails.empty? + .form-group + %label.col-sm-2.control-label{for: "email_inviter_emails"} + = t("email") + .col-sm-10 + = text_field_tag "email_inviter[emails]", @invalid_emails, title: t(".comma_separated_plz"), + placeholder: "foo@bar.com, max@foo.com...", class: "form-control" + #already_sent + = t("invitations.create.note_already_sent", emails: @valid_emails) unless @valid_emails.empty? - .form-group - %label.col-sm-2.control-label{ for: 'email_inviter_locale' } - = t('.language') - .col-sm-10 - = select_tag 'email_inviter[locale]', options_from_collection_for_select(available_language_options, - "second", "first", selected: current_user.language), class: "form-control" + .form-group + %label.col-sm-2.control-label{for: "email_inviter_locale"} + = t(".language") + .col-sm-10 + = select_tag "email_inviter[locale]", options_from_collection_for_select(available_language_options, + "second", "first", selected: current_user.language), class: "form-control" - .form-group - .pull-right.col-md-12 - = submit_tag t('.send_an_invitation'), class: 'btn btn-primary pull-right', - data: {disable_with: t('.sending_invitation')} - .clearfix + .form-group + .pull-right.col-md-12 + = submit_tag t(".send_an_invitation"), class: "btn btn-primary pull-right", + data: {disable_with: t(".sending_invitation")} + .clearfix diff --git a/app/views/shared/_invitations.haml b/app/views/shared/_invitations.haml index 2ddd9e6d9..b69a18c73 100644 --- a/app/views/shared/_invitations.haml +++ b/app/views/shared/_invitations.haml @@ -1,7 +1,9 @@ = t(".share_this") = invite_link(current_user.invitation_code) -.invitations-link.btn.btn-link#invitations-button{"data-toggle" => "modal"} - = t(".by_email") + +- if AppConfig.mail.enable? + .invitations-link.btn.btn-link#invitations-button{"data-toggle" => "modal"} + = t(".by_email") - content_for :after_content do = render "shared/modal", diff --git a/app/views/users/_edit.haml b/app/views/users/_edit.haml index af9aea49b..9a171a923 100644 --- a/app/views/users/_edit.haml +++ b/app/views/users/_edit.haml @@ -117,58 +117,59 @@ = render partial: "post_default" - .row - .col-md-12 - %h3 - = t(".receive_email_notifications") - = form_for "user", url: edit_user_path, html: {method: :put} do |f| - = f.fields_for :email_preferences do |type| - #email_prefs - - if current_user.admin? - = type.label :someone_reported, class: "checkbox-inline" do - = type.check_box :someone_reported, {checked: @email_prefs["someone_reported"]}, false, true - = t(".someone_reported") + - if AppConfig.mail.enable? + .row + .col-md-12 + %h3 + = t(".receive_email_notifications") + = form_for "user", url: edit_user_path, html: {method: :put} do |f| + = f.fields_for :email_preferences do |type| + #email_prefs + - if current_user.admin? + = type.label :someone_reported, class: "checkbox-inline" do + = type.check_box :someone_reported, {checked: @email_prefs["someone_reported"]}, false, true + = t(".someone_reported") - .small-horizontal-spacer + .small-horizontal-spacer - = type.label :started_sharing, class: "checkbox-inline" do - = type.check_box :started_sharing, {checked: @email_prefs["started_sharing"]}, false, true - = t(".started_sharing") - .small-horizontal-spacer + = type.label :started_sharing, class: "checkbox-inline" do + = type.check_box :started_sharing, {checked: @email_prefs["started_sharing"]}, false, true + = t(".started_sharing") + .small-horizontal-spacer - = type.label :mentioned, class: "checkbox-inline" do - = type.check_box :mentioned, {checked: @email_prefs["mentioned"]}, false, true - = t(".mentioned") - .small-horizontal-spacer + = type.label :mentioned, class: "checkbox-inline" do + = type.check_box :mentioned, {checked: @email_prefs["mentioned"]}, false, true + = t(".mentioned") + .small-horizontal-spacer - = type.label :liked, class: "checkbox-inline" do - = type.check_box :liked, {checked: @email_prefs["liked"]}, false, true - = t(".liked") - .small-horizontal-spacer + = type.label :liked, class: "checkbox-inline" do + = type.check_box :liked, {checked: @email_prefs["liked"]}, false, true + = t(".liked") + .small-horizontal-spacer - = type.label :reshared, class: "checkbox-inline" do - = type.check_box :reshared, {checked: @email_prefs["reshared"]}, false, true - = t(".reshared") - .small-horizontal-spacer + = type.label :reshared, class: "checkbox-inline" do + = type.check_box :reshared, {checked: @email_prefs["reshared"]}, false, true + = t(".reshared") + .small-horizontal-spacer - = type.label :comment_on_post, class: "checkbox-inline" do - = type.check_box :comment_on_post, {checked: @email_prefs["comment_on_post"]}, false, true - = t(".comment_on_post") - .small-horizontal-spacer + = type.label :comment_on_post, class: "checkbox-inline" do + = type.check_box :comment_on_post, {checked: @email_prefs["comment_on_post"]}, false, true + = t(".comment_on_post") + .small-horizontal-spacer - = type.label :also_commented, class: "checkbox-inline" do - = type.check_box :also_commented, {checked: @email_prefs["also_commented"]}, false, true - = t(".also_commented") - .small-horizontal-spacer + = type.label :also_commented, class: "checkbox-inline" do + = type.check_box :also_commented, {checked: @email_prefs["also_commented"]}, false, true + = t(".also_commented") + .small-horizontal-spacer - = type.label :private_message, class: "checkbox-inline" do - = type.check_box :private_message, {checked: @email_prefs["private_message"]}, false, true - = t(".private_message") + = type.label :private_message, class: "checkbox-inline" do + = type.check_box :private_message, {checked: @email_prefs["private_message"]}, false, true + = t(".private_message") - .small-horizontal-spacer + .small-horizontal-spacer - .clearfix= f.submit t(".change"), class: "btn btn-primary pull-right", id: "change_email_preferences" - %hr + .clearfix= f.submit t(".change"), class: "btn btn-primary pull-right", id: "change_email_preferences" + %hr .row .col-md-6#account_data From 75f95faebefaa4ec3987ec76eba0caf5011c46ac Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Thu, 17 Nov 2016 16:17:02 +0100 Subject: [PATCH 35/71] Update typeahead.js, use the version from rails-assets.org closes #7192 --- Changelog.md | 1 + Gemfile | 1 + Gemfile.lock | 3 + app/assets/javascripts/main.js | 2 +- vendor/assets/javascripts/typeahead.bundle.js | 2469 ----------------- 5 files changed, 6 insertions(+), 2470 deletions(-) delete mode 100644 vendor/assets/javascripts/typeahead.bundle.js diff --git a/Changelog.md b/Changelog.md index f17dcabaa..42506b441 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,7 @@ * Reduce i18n.load side effects [#7184](https://github.com/diaspora/diaspora/pull/7184) * Force jasmine fails on syntax errors [#7185](https://github.com/diaspora/diaspora/pull/7185) * Don't display mail-related view content if it is disabled in the pod's config [#7190](https://github.com/diaspora/diaspora/pull/7190) +* Use typeahead.js from rails-assets.org [#7192](https://github.com/diaspora/diaspora/pull/7192) ## Bug fixes * Fix fetching comments after fetching likes [#7167](https://github.com/diaspora/diaspora/pull/7167) diff --git a/Gemfile b/Gemfile index 9bf0486c6..bc1cd5791 100644 --- a/Gemfile +++ b/Gemfile @@ -104,6 +104,7 @@ source "https://rails-assets.org" do gem "rails-assets-markdown-it-sup", "1.0.0" gem "rails-assets-highlightjs", "9.7.0" gem "rails-assets-bootstrap-markdown", "2.10.0" + gem "rails-assets-corejs-typeahead", "1.0.1" # jQuery plugins diff --git a/Gemfile.lock b/Gemfile.lock index 73559ce03..70ff9e4fe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -646,6 +646,8 @@ GEM rails-assets-jquery (>= 1.9.1, < 4) rails-assets-bootstrap-markdown (2.10.0) rails-assets-bootstrap (~> 3) + rails-assets-corejs-typeahead (1.0.1) + rails-assets-jquery (>= 1.7) rails-assets-diaspora_jsxc (0.1.5.develop.7) rails-assets-emojione (~> 2.0.1) rails-assets-favico.js (>= 0.3.10, < 0.4) @@ -997,6 +999,7 @@ DEPENDENCIES rails-assets-autosize (= 3.0.17)! rails-assets-blueimp-gallery (= 2.21.3)! rails-assets-bootstrap-markdown (= 2.10.0)! + rails-assets-corejs-typeahead (= 1.0.1)! rails-assets-diaspora_jsxc (= 0.1.5.develop.7)! rails-assets-highlightjs (= 9.7.0)! rails-assets-jasmine-ajax (= 3.2.0)! diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 1d1390820..36541f536 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -30,7 +30,7 @@ //= require markdown-it-sup //= require highlightjs //= require clear-form -//= require typeahead.bundle.js +//= require corejs-typeahead //= require app/app //= require diaspora //= require_tree ./helpers diff --git a/vendor/assets/javascripts/typeahead.bundle.js b/vendor/assets/javascripts/typeahead.bundle.js deleted file mode 100644 index b68ec30f3..000000000 --- a/vendor/assets/javascripts/typeahead.bundle.js +++ /dev/null @@ -1,2469 +0,0 @@ -/*! - * typeahead.js 0.11.1 - * https://github.com/twitter/typeahead.js - * Copyright 2013-2016 Twitter, Inc. and other contributors; Licensed MIT - */ - -(function(root, factory) { - if (typeof define === "function" && define.amd) { - define([ "jquery" ], function(a0) { - return root["Bloodhound"] = factory(a0); - }); - } else if (typeof exports === "object") { - module.exports = factory(require("jquery")); - } else { - root["Bloodhound"] = factory(jQuery); - } -})(this, function($) { - var _ = function() { - "use strict"; - return { - isMsie: function() { - return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; - }, - isBlankString: function(str) { - return !str || /^\s*$/.test(str); - }, - escapeRegExChars: function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - isString: function(obj) { - return typeof obj === "string"; - }, - isNumber: function(obj) { - return typeof obj === "number"; - }, - isArray: $.isArray, - isFunction: $.isFunction, - isObject: $.isPlainObject, - isUndefined: function(obj) { - return typeof obj === "undefined"; - }, - isElement: function(obj) { - return !!(obj && obj.nodeType === 1); - }, - isJQuery: function(obj) { - return obj instanceof $; - }, - toStr: function toStr(s) { - return _.isUndefined(s) || s === null ? "" : s + ""; - }, - bind: $.proxy, - each: function(collection, cb) { - $.each(collection, reverseArgs); - function reverseArgs(index, value) { - return cb(value, index); - } - }, - map: $.map, - filter: $.grep, - every: function(obj, test) { - var result = true; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (!(result = test.call(null, val, key, obj))) { - return false; - } - }); - return !!result; - }, - some: function(obj, test) { - var result = false; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (result = test.call(null, val, key, obj)) { - return false; - } - }); - return !!result; - }, - mixin: $.extend, - identity: function(x) { - return x; - }, - clone: function(obj) { - return $.extend(true, {}, obj); - }, - getIdGenerator: function() { - var counter = 0; - return function() { - return counter++; - }; - }, - templatify: function templatify(obj) { - return $.isFunction(obj) ? obj : template; - function template() { - return String(obj); - } - }, - defer: function(fn) { - setTimeout(fn, 0); - }, - debounce: function(func, wait, immediate) { - var timeout, result; - return function() { - var context = this, args = arguments, later, callNow; - later = function() { - timeout = null; - if (!immediate) { - result = func.apply(context, args); - } - }; - callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - result = func.apply(context, args); - } - return result; - }; - }, - throttle: function(func, wait) { - var context, args, timeout, result, previous, later; - previous = 0; - later = function() { - previous = new Date(); - timeout = null; - result = func.apply(context, args); - }; - return function() { - var now = new Date(), remaining = wait - (now - previous); - context = this; - args = arguments; - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(context, args); - } else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; - }, - stringify: function(val) { - return _.isString(val) ? val : JSON.stringify(val); - }, - noop: function() {} - }; - }(); - var VERSION = "0.11.1"; - var tokenizers = function() { - "use strict"; - return { - nonword: nonword, - whitespace: whitespace, - obj: { - nonword: getObjTokenizer(nonword), - whitespace: getObjTokenizer(whitespace) - } - }; - function whitespace(str) { - str = _.toStr(str); - return str ? str.split(/\s+/) : []; - } - function nonword(str) { - str = _.toStr(str); - return str ? str.split(/\W+/) : []; - } - function getObjTokenizer(tokenizer) { - return function setKey(keys) { - keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0); - return function tokenize(o) { - var tokens = []; - _.each(keys, function(k) { - tokens = tokens.concat(tokenizer(_.toStr(o[k]))); - }); - return tokens; - }; - }; - } - }(); - var LruCache = function() { - "use strict"; - function LruCache(maxSize) { - this.maxSize = _.isNumber(maxSize) ? maxSize : 100; - this.reset(); - if (this.maxSize <= 0) { - this.set = this.get = $.noop; - } - } - _.mixin(LruCache.prototype, { - set: function set(key, val) { - var tailItem = this.list.tail, node; - if (this.size >= this.maxSize) { - this.list.remove(tailItem); - delete this.hash[tailItem.key]; - this.size--; - } - if (node = this.hash[key]) { - node.val = val; - this.list.moveToFront(node); - } else { - node = new Node(key, val); - this.list.add(node); - this.hash[key] = node; - this.size++; - } - }, - get: function get(key) { - var node = this.hash[key]; - if (node) { - this.list.moveToFront(node); - return node.val; - } - }, - reset: function reset() { - this.size = 0; - this.hash = {}; - this.list = new List(); - } - }); - function List() { - this.head = this.tail = null; - } - _.mixin(List.prototype, { - add: function add(node) { - if (this.head) { - node.next = this.head; - this.head.prev = node; - } - this.head = node; - this.tail = this.tail || node; - }, - remove: function remove(node) { - node.prev ? node.prev.next = node.next : this.head = node.next; - node.next ? node.next.prev = node.prev : this.tail = node.prev; - }, - moveToFront: function(node) { - this.remove(node); - this.add(node); - } - }); - function Node(key, val) { - this.key = key; - this.val = val; - this.prev = this.next = null; - } - return LruCache; - }(); - var PersistentStorage = function() { - "use strict"; - var LOCAL_STORAGE; - try { - LOCAL_STORAGE = window.localStorage; - LOCAL_STORAGE.setItem("~~~", "!"); - LOCAL_STORAGE.removeItem("~~~"); - } catch (err) { - LOCAL_STORAGE = null; - } - function PersistentStorage(namespace, override) { - this.prefix = [ "__", namespace, "__" ].join(""); - this.ttlKey = "__ttl__"; - this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix)); - this.ls = override || LOCAL_STORAGE; - !this.ls && this._noop(); - } - _.mixin(PersistentStorage.prototype, { - _prefix: function(key) { - return this.prefix + key; - }, - _ttlKey: function(key) { - return this._prefix(key) + this.ttlKey; - }, - _noop: function() { - this.get = this.set = this.remove = this.clear = this.isExpired = _.noop; - }, - _safeSet: function(key, val) { - try { - this.ls.setItem(key, val); - } catch (err) { - if (err.name === "QuotaExceededError") { - this.clear(); - this._noop(); - } - } - }, - get: function(key) { - if (this.isExpired(key)) { - this.remove(key); - } - return decode(this.ls.getItem(this._prefix(key))); - }, - set: function(key, val, ttl) { - if (_.isNumber(ttl)) { - this._safeSet(this._ttlKey(key), encode(now() + ttl)); - } else { - this.ls.removeItem(this._ttlKey(key)); - } - return this._safeSet(this._prefix(key), encode(val)); - }, - remove: function(key) { - this.ls.removeItem(this._ttlKey(key)); - this.ls.removeItem(this._prefix(key)); - return this; - }, - clear: function() { - var i, keys = gatherMatchingKeys(this.keyMatcher); - for (i = keys.length; i--; ) { - this.remove(keys[i]); - } - return this; - }, - isExpired: function(key) { - var ttl = decode(this.ls.getItem(this._ttlKey(key))); - return _.isNumber(ttl) && now() > ttl ? true : false; - } - }); - return PersistentStorage; - function now() { - return new Date().getTime(); - } - function encode(val) { - return JSON.stringify(_.isUndefined(val) ? null : val); - } - function decode(val) { - return $.parseJSON(val); - } - function gatherMatchingKeys(keyMatcher) { - var i, key, keys = [], len = LOCAL_STORAGE.length; - for (i = 0; i < len; i++) { - if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) { - keys.push(key.replace(keyMatcher, "")); - } - } - return keys; - } - }(); - var Transport = function() { - "use strict"; - var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10); - function Transport(o) { - o = o || {}; - this.cancelled = false; - this.lastReq = null; - this._send = o.transport; - this._get = o.limiter ? o.limiter(this._get) : this._get; - this._cache = o.cache === false ? new LruCache(0) : sharedCache; - } - Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { - maxPendingRequests = num; - }; - Transport.resetCache = function resetCache() { - sharedCache.reset(); - }; - _.mixin(Transport.prototype, { - _fingerprint: function fingerprint(o) { - o = o || {}; - return o.url + o.type + $.param(o.data || {}); - }, - _get: function(o, cb) { - var that = this, fingerprint, jqXhr; - fingerprint = this._fingerprint(o); - if (this.cancelled || fingerprint !== this.lastReq) { - return; - } - if (jqXhr = pendingRequests[fingerprint]) { - jqXhr.done(done).fail(fail); - } else if (pendingRequestsCount < maxPendingRequests) { - pendingRequestsCount++; - pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always); - } else { - this.onDeckRequestArgs = [].slice.call(arguments, 0); - } - function done(resp) { - cb(null, resp); - that._cache.set(fingerprint, resp); - } - function fail() { - cb(true); - } - function always() { - pendingRequestsCount--; - delete pendingRequests[fingerprint]; - if (that.onDeckRequestArgs) { - that._get.apply(that, that.onDeckRequestArgs); - that.onDeckRequestArgs = null; - } - } - }, - get: function(o, cb) { - var resp, fingerprint; - cb = cb || $.noop; - o = _.isString(o) ? { - url: o - } : o || {}; - fingerprint = this._fingerprint(o); - this.cancelled = false; - this.lastReq = fingerprint; - if (resp = this._cache.get(fingerprint)) { - cb(null, resp); - } else { - this._get(o, cb); - } - }, - cancel: function() { - this.cancelled = true; - } - }); - return Transport; - }(); - var SearchIndex = window.SearchIndex = function() { - "use strict"; - var CHILDREN = "c", IDS = "i"; - function SearchIndex(o) { - o = o || {}; - if (!o.datumTokenizer || !o.queryTokenizer) { - $.error("datumTokenizer and queryTokenizer are both required"); - } - this.identify = o.identify || _.stringify; - this.datumTokenizer = o.datumTokenizer; - this.queryTokenizer = o.queryTokenizer; - this.matchAnyQueryToken = o.matchAnyQueryToken; - this.reset(); - } - _.mixin(SearchIndex.prototype, { - bootstrap: function bootstrap(o) { - this.datums = o.datums; - this.trie = o.trie; - }, - add: function(data) { - var that = this; - data = _.isArray(data) ? data : [ data ]; - _.each(data, function(datum) { - var id, tokens; - that.datums[id = that.identify(datum)] = datum; - tokens = normalizeTokens(that.datumTokenizer(datum)); - _.each(tokens, function(token) { - var node, chars, ch; - node = that.trie; - chars = token.split(""); - while (ch = chars.shift()) { - node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode()); - node[IDS].push(id); - } - }); - }); - }, - get: function get(ids) { - var that = this; - return _.map(ids, function(id) { - return that.datums[id]; - }); - }, - search: function search(query) { - var that = this, tokens, matches; - tokens = normalizeTokens(this.queryTokenizer(query)); - _.each(tokens, function(token) { - var node, chars, ch, ids; - if (matches && matches.length === 0 && !that.matchAnyQueryToken) { - return false; - } - node = that.trie; - chars = token.split(""); - while (node && (ch = chars.shift())) { - node = node[CHILDREN][ch]; - } - if (node && chars.length === 0) { - ids = node[IDS].slice(0); - matches = matches ? getIntersection(matches, ids) : ids; - } else { - if (!that.matchAnyQueryToken) { - matches = []; - return false; - } - } - }); - return matches ? _.map(unique(matches), function(id) { - return that.datums[id]; - }) : []; - }, - all: function all() { - var values = []; - for (var key in this.datums) { - values.push(this.datums[key]); - } - return values; - }, - reset: function reset() { - this.datums = {}; - this.trie = newNode(); - }, - serialize: function serialize() { - return { - datums: this.datums, - trie: this.trie - }; - } - }); - return SearchIndex; - function normalizeTokens(tokens) { - tokens = _.filter(tokens, function(token) { - return !!token; - }); - tokens = _.map(tokens, function(token) { - return token.toLowerCase(); - }); - return tokens; - } - function newNode() { - var node = {}; - node[IDS] = []; - node[CHILDREN] = {}; - return node; - } - function unique(array) { - var seen = {}, uniques = []; - for (var i = 0, len = array.length; i < len; i++) { - if (!seen[array[i]]) { - seen[array[i]] = true; - uniques.push(array[i]); - } - } - return uniques; - } - function getIntersection(arrayA, arrayB) { - var ai = 0, bi = 0, intersection = []; - arrayA = arrayA.sort(); - arrayB = arrayB.sort(); - var lenArrayA = arrayA.length, lenArrayB = arrayB.length; - while (ai < lenArrayA && bi < lenArrayB) { - if (arrayA[ai] < arrayB[bi]) { - ai++; - } else if (arrayA[ai] > arrayB[bi]) { - bi++; - } else { - intersection.push(arrayA[ai]); - ai++; - bi++; - } - } - return intersection; - } - }(); - var Prefetch = function() { - "use strict"; - var keys; - keys = { - data: "data", - protocol: "protocol", - thumbprint: "thumbprint" - }; - function Prefetch(o) { - this.url = o.url; - this.ttl = o.ttl; - this.cache = o.cache; - this.prepare = o.prepare; - this.transform = o.transform; - this.transport = o.transport; - this.thumbprint = o.thumbprint; - this.storage = new PersistentStorage(o.cacheKey); - } - _.mixin(Prefetch.prototype, { - _settings: function settings() { - return { - url: this.url, - type: "GET", - dataType: "json" - }; - }, - store: function store(data) { - if (!this.cache) { - return; - } - this.storage.set(keys.data, data, this.ttl); - this.storage.set(keys.protocol, location.protocol, this.ttl); - this.storage.set(keys.thumbprint, this.thumbprint, this.ttl); - }, - fromCache: function fromCache() { - var stored = {}, isExpired; - if (!this.cache) { - return null; - } - stored.data = this.storage.get(keys.data); - stored.protocol = this.storage.get(keys.protocol); - stored.thumbprint = this.storage.get(keys.thumbprint); - isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol; - return stored.data && !isExpired ? stored.data : null; - }, - fromNetwork: function(cb) { - var that = this, settings; - if (!cb) { - return; - } - settings = this.prepare(this._settings()); - this.transport(settings).fail(onError).done(onResponse); - function onError() { - cb(true); - } - function onResponse(resp) { - cb(null, that.transform(resp)); - } - }, - clear: function clear() { - this.storage.clear(); - return this; - } - }); - return Prefetch; - }(); - var Remote = function() { - "use strict"; - function Remote(o) { - this.url = o.url; - this.prepare = o.prepare; - this.transform = o.transform; - this.indexResponse = o.indexResponse; - this.transport = new Transport({ - cache: o.cache, - limiter: o.limiter, - transport: o.transport - }); - } - _.mixin(Remote.prototype, { - _settings: function settings() { - return { - url: this.url, - type: "GET", - dataType: "json" - }; - }, - get: function get(query, cb) { - var that = this, settings; - if (!cb) { - return; - } - query = query || ""; - settings = this.prepare(query, this._settings()); - return this.transport.get(settings, onResponse); - function onResponse(err, resp) { - err ? cb([]) : cb(that.transform(resp)); - } - }, - cancelLastRequest: function cancelLastRequest() { - this.transport.cancel(); - } - }); - return Remote; - }(); - var oParser = function() { - "use strict"; - return function parse(o) { - var defaults, sorter; - defaults = { - initialize: true, - identify: _.stringify, - datumTokenizer: null, - queryTokenizer: null, - matchAnyQueryToken: false, - sufficient: 5, - indexRemote: false, - sorter: null, - local: [], - prefetch: null, - remote: null - }; - o = _.mixin(defaults, o || {}); - !o.datumTokenizer && $.error("datumTokenizer is required"); - !o.queryTokenizer && $.error("queryTokenizer is required"); - sorter = o.sorter; - o.sorter = sorter ? function(x) { - return x.sort(sorter); - } : _.identity; - o.local = _.isFunction(o.local) ? o.local() : o.local; - o.prefetch = parsePrefetch(o.prefetch); - o.remote = parseRemote(o.remote); - return o; - }; - function parsePrefetch(o) { - var defaults; - if (!o) { - return null; - } - defaults = { - url: null, - ttl: 24 * 60 * 60 * 1e3, - cache: true, - cacheKey: null, - thumbprint: "", - prepare: _.identity, - transform: _.identity, - transport: null - }; - o = _.isString(o) ? { - url: o - } : o; - o = _.mixin(defaults, o); - !o.url && $.error("prefetch requires url to be set"); - o.transform = o.filter || o.transform; - o.cacheKey = o.cacheKey || o.url; - o.thumbprint = VERSION + o.thumbprint; - o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; - return o; - } - function parseRemote(o) { - var defaults; - if (!o) { - return; - } - defaults = { - url: null, - cache: true, - prepare: null, - replace: null, - wildcard: null, - limiter: null, - rateLimitBy: "debounce", - rateLimitWait: 300, - transform: _.identity, - transport: null - }; - o = _.isString(o) ? { - url: o - } : o; - o = _.mixin(defaults, o); - !o.url && $.error("remote requires url to be set"); - o.transform = o.filter || o.transform; - o.prepare = toRemotePrepare(o); - o.limiter = toLimiter(o); - o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; - delete o.replace; - delete o.wildcard; - delete o.rateLimitBy; - delete o.rateLimitWait; - return o; - } - function toRemotePrepare(o) { - var prepare, replace, wildcard; - prepare = o.prepare; - replace = o.replace; - wildcard = o.wildcard; - if (prepare) { - return prepare; - } - if (replace) { - prepare = prepareByReplace; - } else if (o.wildcard) { - prepare = prepareByWildcard; - } else { - prepare = idenityPrepare; - } - return prepare; - function prepareByReplace(query, settings) { - settings.url = replace(settings.url, query); - return settings; - } - function prepareByWildcard(query, settings) { - settings.url = settings.url.replace(wildcard, encodeURIComponent(query)); - return settings; - } - function idenityPrepare(query, settings) { - return settings; - } - } - function toLimiter(o) { - var limiter, method, wait; - limiter = o.limiter; - method = o.rateLimitBy; - wait = o.rateLimitWait; - if (!limiter) { - limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait); - } - return limiter; - function debounce(wait) { - return function debounce(fn) { - return _.debounce(fn, wait); - }; - } - function throttle(wait) { - return function throttle(fn) { - return _.throttle(fn, wait); - }; - } - } - function callbackToDeferred(fn) { - return function wrapper(o) { - var deferred = $.Deferred(); - fn(o, onSuccess, onError); - return deferred; - function onSuccess(resp) { - _.defer(function() { - deferred.resolve(resp); - }); - } - function onError(err) { - _.defer(function() { - deferred.reject(err); - }); - } - }; - } - }(); - var Bloodhound = function() { - "use strict"; - var old; - old = window && window.Bloodhound; - function Bloodhound(o) { - o = oParser(o); - this.sorter = o.sorter; - this.identify = o.identify; - this.sufficient = o.sufficient; - this.indexRemote = o.indexRemote; - this.local = o.local; - this.remote = o.remote ? new Remote(o.remote) : null; - this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null; - this.index = new SearchIndex({ - identify: this.identify, - datumTokenizer: o.datumTokenizer, - queryTokenizer: o.queryTokenizer - }); - o.initialize !== false && this.initialize(); - } - Bloodhound.noConflict = function noConflict() { - window && (window.Bloodhound = old); - return Bloodhound; - }; - Bloodhound.tokenizers = tokenizers; - _.mixin(Bloodhound.prototype, { - __ttAdapter: function ttAdapter() { - var that = this; - return this.remote ? withAsync : withoutAsync; - function withAsync(query, sync, async) { - return that.search(query, sync, async); - } - function withoutAsync(query, sync) { - return that.search(query, sync); - } - }, - _loadPrefetch: function loadPrefetch() { - var that = this, deferred, serialized; - deferred = $.Deferred(); - if (!this.prefetch) { - deferred.resolve(); - } else if (serialized = this.prefetch.fromCache()) { - this.index.bootstrap(serialized); - deferred.resolve(); - } else { - this.prefetch.fromNetwork(done); - } - return deferred.promise(); - function done(err, data) { - if (err) { - return deferred.reject(); - } - that.add(data); - that.prefetch.store(that.index.serialize()); - deferred.resolve(); - } - }, - _initialize: function initialize() { - var that = this, deferred; - this.clear(); - (this.initPromise = this._loadPrefetch()).done(addLocalToIndex); - return this.initPromise; - function addLocalToIndex() { - that.add(that.local); - } - }, - initialize: function initialize(force) { - return !this.initPromise || force ? this._initialize() : this.initPromise; - }, - add: function add(data) { - this.index.add(data); - return this; - }, - get: function get(ids) { - ids = _.isArray(ids) ? ids : [].slice.call(arguments); - return this.index.get(ids); - }, - search: function search(query, sync, async) { - var that = this, local; - sync = sync || _.noop; - async = async || _.noop; - local = this.sorter(this.index.search(query)); - sync(this.remote ? local.slice() : local); - if (this.remote && local.length < this.sufficient) { - this.remote.get(query, processRemote); - } else if (this.remote) { - this.remote.cancelLastRequest(); - } - return this; - function processRemote(remote) { - var nonDuplicates = []; - _.each(remote, function(r) { - !_.some(local, function(l) { - return that.identify(r) === that.identify(l); - }) && nonDuplicates.push(r); - }); - that.indexRemote && that.add(nonDuplicates); - async(nonDuplicates); - } - }, - all: function all() { - return this.index.all(); - }, - clear: function clear() { - this.index.reset(); - return this; - }, - clearPrefetchCache: function clearPrefetchCache() { - this.prefetch && this.prefetch.clear(); - return this; - }, - clearRemoteCache: function clearRemoteCache() { - Transport.resetCache(); - return this; - }, - ttAdapter: function ttAdapter() { - return this.__ttAdapter(); - } - }); - return Bloodhound; - }(); - return Bloodhound; -}); - -(function(root, factory) { - if (typeof define === "function" && define.amd) { - define([ "jquery" ], function(a0) { - return factory(a0); - }); - } else if (typeof exports === "object") { - module.exports = factory(require("jquery")); - } else { - factory(jQuery); - } -})(this, function($) { - var _ = function() { - "use strict"; - return { - isMsie: function() { - return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; - }, - isBlankString: function(str) { - return !str || /^\s*$/.test(str); - }, - escapeRegExChars: function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - isString: function(obj) { - return typeof obj === "string"; - }, - isNumber: function(obj) { - return typeof obj === "number"; - }, - isArray: $.isArray, - isFunction: $.isFunction, - isObject: $.isPlainObject, - isUndefined: function(obj) { - return typeof obj === "undefined"; - }, - isElement: function(obj) { - return !!(obj && obj.nodeType === 1); - }, - isJQuery: function(obj) { - return obj instanceof $; - }, - toStr: function toStr(s) { - return _.isUndefined(s) || s === null ? "" : s + ""; - }, - bind: $.proxy, - each: function(collection, cb) { - $.each(collection, reverseArgs); - function reverseArgs(index, value) { - return cb(value, index); - } - }, - map: $.map, - filter: $.grep, - every: function(obj, test) { - var result = true; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (!(result = test.call(null, val, key, obj))) { - return false; - } - }); - return !!result; - }, - some: function(obj, test) { - var result = false; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (result = test.call(null, val, key, obj)) { - return false; - } - }); - return !!result; - }, - mixin: $.extend, - identity: function(x) { - return x; - }, - clone: function(obj) { - return $.extend(true, {}, obj); - }, - getIdGenerator: function() { - var counter = 0; - return function() { - return counter++; - }; - }, - templatify: function templatify(obj) { - return $.isFunction(obj) ? obj : template; - function template() { - return String(obj); - } - }, - defer: function(fn) { - setTimeout(fn, 0); - }, - debounce: function(func, wait, immediate) { - var timeout, result; - return function() { - var context = this, args = arguments, later, callNow; - later = function() { - timeout = null; - if (!immediate) { - result = func.apply(context, args); - } - }; - callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - result = func.apply(context, args); - } - return result; - }; - }, - throttle: function(func, wait) { - var context, args, timeout, result, previous, later; - previous = 0; - later = function() { - previous = new Date(); - timeout = null; - result = func.apply(context, args); - }; - return function() { - var now = new Date(), remaining = wait - (now - previous); - context = this; - args = arguments; - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(context, args); - } else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; - }, - stringify: function(val) { - return _.isString(val) ? val : JSON.stringify(val); - }, - noop: function() {} - }; - }(); - var WWW = function() { - "use strict"; - var defaultClassNames = { - wrapper: "twitter-typeahead", - input: "tt-input", - hint: "tt-hint", - menu: "tt-menu", - dataset: "tt-dataset", - suggestion: "tt-suggestion", - selectable: "tt-selectable", - empty: "tt-empty", - open: "tt-open", - cursor: "tt-cursor", - highlight: "tt-highlight" - }; - return build; - function build(o) { - var www, classes; - classes = _.mixin({}, defaultClassNames, o); - www = { - css: buildCss(), - classes: classes, - html: buildHtml(classes), - selectors: buildSelectors(classes) - }; - return { - css: www.css, - html: www.html, - classes: www.classes, - selectors: www.selectors, - mixin: function(o) { - _.mixin(o, www); - } - }; - } - function buildHtml(c) { - return { - wrapper: '', - menu: '
' - }; - } - function buildSelectors(classes) { - var selectors = {}; - _.each(classes, function(v, k) { - selectors[k] = "." + v; - }); - return selectors; - } - function buildCss() { - var css = { - wrapper: { - position: "relative", - display: "inline-block" - }, - hint: { - position: "absolute", - top: "0", - left: "0", - borderColor: "transparent", - boxShadow: "none", - opacity: "1" - }, - input: { - position: "relative", - verticalAlign: "top", - backgroundColor: "transparent" - }, - inputWithNoHint: { - position: "relative", - verticalAlign: "top" - }, - menu: { - position: "absolute", - top: "100%", - left: "0", - zIndex: "100", - display: "none" - }, - ltr: { - left: "0", - right: "auto" - }, - rtl: { - left: "auto", - right: " 0" - } - }; - if (_.isMsie()) { - _.mixin(css.input, { - backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" - }); - } - return css; - } - }(); - var EventBus = function() { - "use strict"; - var namespace, deprecationMap; - namespace = "typeahead:"; - deprecationMap = { - render: "rendered", - cursorchange: "cursorchanged", - select: "selected", - autocomplete: "autocompleted" - }; - function EventBus(o) { - if (!o || !o.el) { - $.error("EventBus initialized without el"); - } - this.$el = $(o.el); - } - _.mixin(EventBus.prototype, { - _trigger: function(type, args) { - var $e; - $e = $.Event(namespace + type); - (args = args || []).unshift($e); - this.$el.trigger.apply(this.$el, args); - return $e; - }, - before: function(type) { - var args, $e; - args = [].slice.call(arguments, 1); - $e = this._trigger("before" + type, args); - return $e.isDefaultPrevented(); - }, - trigger: function(type) { - var deprecatedType; - this._trigger(type, [].slice.call(arguments, 1)); - if (deprecatedType = deprecationMap[type]) { - this._trigger(deprecatedType, [].slice.call(arguments, 1)); - } - } - }); - return EventBus; - }(); - var EventEmitter = function() { - "use strict"; - var splitter = /\s+/, nextTick = getNextTick(); - return { - onSync: onSync, - onAsync: onAsync, - off: off, - trigger: trigger - }; - function on(method, types, cb, context) { - var type; - if (!cb) { - return this; - } - types = types.split(splitter); - cb = context ? bindContext(cb, context) : cb; - this._callbacks = this._callbacks || {}; - while (type = types.shift()) { - this._callbacks[type] = this._callbacks[type] || { - sync: [], - async: [] - }; - this._callbacks[type][method].push(cb); - } - return this; - } - function onAsync(types, cb, context) { - return on.call(this, "async", types, cb, context); - } - function onSync(types, cb, context) { - return on.call(this, "sync", types, cb, context); - } - function off(types) { - var type; - if (!this._callbacks) { - return this; - } - types = types.split(splitter); - while (type = types.shift()) { - delete this._callbacks[type]; - } - return this; - } - function trigger(types) { - var type, callbacks, args, syncFlush, asyncFlush; - if (!this._callbacks) { - return this; - } - types = types.split(splitter); - args = [].slice.call(arguments, 1); - while ((type = types.shift()) && (callbacks = this._callbacks[type])) { - syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); - asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); - syncFlush() && nextTick(asyncFlush); - } - return this; - } - function getFlush(callbacks, context, args) { - return flush; - function flush() { - var cancelled; - for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { - cancelled = callbacks[i].apply(context, args) === false; - } - return !cancelled; - } - } - function getNextTick() { - var nextTickFn; - if (window.setImmediate) { - nextTickFn = function nextTickSetImmediate(fn) { - setImmediate(function() { - fn(); - }); - }; - } else { - nextTickFn = function nextTickSetTimeout(fn) { - setTimeout(function() { - fn(); - }, 0); - }; - } - return nextTickFn; - } - function bindContext(fn, context) { - return fn.bind ? fn.bind(context) : function() { - fn.apply(context, [].slice.call(arguments, 0)); - }; - } - }(); - var highlight = function(doc) { - "use strict"; - var defaults = { - node: null, - pattern: null, - tagName: "strong", - className: null, - wordsOnly: false, - caseSensitive: false - }; - return function hightlight(o) { - var regex; - o = _.mixin({}, defaults, o); - if (!o.node || !o.pattern) { - return; - } - o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; - regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); - traverse(o.node, hightlightTextNode); - function hightlightTextNode(textNode) { - var match, patternNode, wrapperNode; - if (match = regex.exec(textNode.data)) { - wrapperNode = doc.createElement(o.tagName); - o.className && (wrapperNode.className = o.className); - patternNode = textNode.splitText(match.index); - patternNode.splitText(match[0].length); - wrapperNode.appendChild(patternNode.cloneNode(true)); - textNode.parentNode.replaceChild(wrapperNode, patternNode); - } - return !!match; - } - function traverse(el, hightlightTextNode) { - var childNode, TEXT_NODE_TYPE = 3; - for (var i = 0; i < el.childNodes.length; i++) { - childNode = el.childNodes[i]; - if (childNode.nodeType === TEXT_NODE_TYPE) { - i += hightlightTextNode(childNode) ? 1 : 0; - } else { - traverse(childNode, hightlightTextNode); - } - } - } - }; - function getRegex(patterns, caseSensitive, wordsOnly) { - var escapedPatterns = [], regexStr; - for (var i = 0, len = patterns.length; i < len; i++) { - escapedPatterns.push(_.escapeRegExChars(patterns[i])); - } - regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; - return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); - } - }(window.document); - var Input = function() { - "use strict"; - var specialKeyCodeMap; - specialKeyCodeMap = { - 9: "tab", - 27: "esc", - 37: "left", - 39: "right", - 13: "enter", - 38: "up", - 40: "down" - }; - function Input(o, www) { - o = o || {}; - if (!o.input) { - $.error("input is missing"); - } - www.mixin(this); - this.$hint = $(o.hint); - this.$input = $(o.input); - this.query = this.$input.val(); - this.queryWhenFocused = this.hasFocus() ? this.query : null; - this.$overflowHelper = buildOverflowHelper(this.$input); - this._checkLanguageDirection(); - if (this.$hint.length === 0) { - this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; - } - } - Input.normalizeQuery = function(str) { - return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " "); - }; - _.mixin(Input.prototype, EventEmitter, { - _onBlur: function onBlur() { - this.resetInputValue(); - this.trigger("blurred"); - }, - _onFocus: function onFocus() { - this.queryWhenFocused = this.query; - this.trigger("focused"); - }, - _onKeydown: function onKeydown($e) { - var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; - this._managePreventDefault(keyName, $e); - if (keyName && this._shouldTrigger(keyName, $e)) { - this.trigger(keyName + "Keyed", $e); - } - }, - _onInput: function onInput() { - this._setQuery(this.getInputValue()); - this.clearHintIfInvalid(); - this._checkLanguageDirection(); - }, - _managePreventDefault: function managePreventDefault(keyName, $e) { - var preventDefault; - switch (keyName) { - case "up": - case "down": - preventDefault = !withModifier($e); - break; - - default: - preventDefault = false; - } - preventDefault && $e.preventDefault(); - }, - _shouldTrigger: function shouldTrigger(keyName, $e) { - var trigger; - switch (keyName) { - case "tab": - trigger = !withModifier($e); - break; - - default: - trigger = true; - } - return trigger; - }, - _checkLanguageDirection: function checkLanguageDirection() { - var dir = (this.$input.css("direction") || "ltr").toLowerCase(); - if (this.dir !== dir) { - this.dir = dir; - this.$hint.attr("dir", dir); - this.trigger("langDirChanged", dir); - } - }, - _setQuery: function setQuery(val, silent) { - var areEquivalent, hasDifferentWhitespace; - areEquivalent = areQueriesEquivalent(val, this.query); - hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false; - this.query = val; - if (!silent && !areEquivalent) { - this.trigger("queryChanged", this.query); - } else if (!silent && hasDifferentWhitespace) { - this.trigger("whitespaceChanged", this.query); - } - }, - bind: function() { - var that = this, onBlur, onFocus, onKeydown, onInput; - onBlur = _.bind(this._onBlur, this); - onFocus = _.bind(this._onFocus, this); - onKeydown = _.bind(this._onKeydown, this); - onInput = _.bind(this._onInput, this); - this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); - if (!_.isMsie() || _.isMsie() > 9) { - this.$input.on("input.tt", onInput); - } else { - this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { - if (specialKeyCodeMap[$e.which || $e.keyCode]) { - return; - } - _.defer(_.bind(that._onInput, that, $e)); - }); - } - return this; - }, - focus: function focus() { - this.$input.focus(); - }, - blur: function blur() { - this.$input.blur(); - }, - getLangDir: function getLangDir() { - return this.dir; - }, - getQuery: function getQuery() { - return this.query || ""; - }, - setQuery: function setQuery(val, silent) { - this.setInputValue(val); - this._setQuery(val, silent); - }, - hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() { - return this.query !== this.queryWhenFocused; - }, - getInputValue: function getInputValue() { - return this.$input.val(); - }, - setInputValue: function setInputValue(value) { - this.$input.val(value); - this.clearHintIfInvalid(); - this._checkLanguageDirection(); - }, - resetInputValue: function resetInputValue() { - this.setInputValue(this.query); - }, - getHint: function getHint() { - return this.$hint.val(); - }, - setHint: function setHint(value) { - this.$hint.val(value); - }, - clearHint: function clearHint() { - this.setHint(""); - }, - clearHintIfInvalid: function clearHintIfInvalid() { - var val, hint, valIsPrefixOfHint, isValid; - val = this.getInputValue(); - hint = this.getHint(); - valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; - isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); - !isValid && this.clearHint(); - }, - hasFocus: function hasFocus() { - return this.$input.is(":focus"); - }, - hasOverflow: function hasOverflow() { - var constraint = this.$input.width() - 2; - this.$overflowHelper.text(this.getInputValue()); - return this.$overflowHelper.width() >= constraint; - }, - isCursorAtEnd: function() { - var valueLength, selectionStart, range; - valueLength = this.$input.val().length; - selectionStart = this.$input[0].selectionStart; - if (_.isNumber(selectionStart)) { - return selectionStart === valueLength; - } else if (document.selection) { - range = document.selection.createRange(); - range.moveStart("character", -valueLength); - return valueLength === range.text.length; - } - return true; - }, - destroy: function destroy() { - this.$hint.off(".tt"); - this.$input.off(".tt"); - this.$overflowHelper.remove(); - this.$hint = this.$input = this.$overflowHelper = $("
"); - } - }); - return Input; - function buildOverflowHelper($input) { - return $('').css({ - position: "absolute", - visibility: "hidden", - whiteSpace: "pre", - fontFamily: $input.css("font-family"), - fontSize: $input.css("font-size"), - fontStyle: $input.css("font-style"), - fontVariant: $input.css("font-variant"), - fontWeight: $input.css("font-weight"), - wordSpacing: $input.css("word-spacing"), - letterSpacing: $input.css("letter-spacing"), - textIndent: $input.css("text-indent"), - textRendering: $input.css("text-rendering"), - textTransform: $input.css("text-transform") - }).insertAfter($input); - } - function areQueriesEquivalent(a, b) { - return Input.normalizeQuery(a) === Input.normalizeQuery(b); - } - function withModifier($e) { - return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; - } - }(); - var Dataset = function() { - "use strict"; - var keys, nameGenerator; - keys = { - val: "tt-selectable-display", - obj: "tt-selectable-object" - }; - nameGenerator = _.getIdGenerator(); - function Dataset(o, www) { - o = o || {}; - o.templates = o.templates || {}; - o.templates.notFound = o.templates.notFound || o.templates.empty; - if (!o.source) { - $.error("missing source"); - } - if (!o.node) { - $.error("missing node"); - } - if (o.name && !isValidName(o.name)) { - $.error("invalid dataset name: " + o.name); - } - www.mixin(this); - this.highlight = !!o.highlight; - this.name = o.name || nameGenerator(); - this.limit = o.limit || 5; - this.displayFn = getDisplayFn(o.display || o.displayKey); - this.templates = getTemplates(o.templates, this.displayFn); - this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source; - this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async; - this._resetLastSuggestion(); - this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name); - } - Dataset.extractData = function extractData(el) { - var $el = $(el); - if ($el.data(keys.obj)) { - return { - val: $el.data(keys.val) || "", - obj: $el.data(keys.obj) || null - }; - } - return null; - }; - _.mixin(Dataset.prototype, EventEmitter, { - _overwrite: function overwrite(query, suggestions) { - suggestions = suggestions || []; - if (suggestions.length) { - this._renderSuggestions(query, suggestions); - } else if (this.async && this.templates.pending) { - this._renderPending(query); - } else if (!this.async && this.templates.notFound) { - this._renderNotFound(query); - } else { - this._empty(); - } - this.trigger("rendered", this.name, suggestions, false); - }, - _append: function append(query, suggestions) { - suggestions = suggestions || []; - if (suggestions.length && this.$lastSuggestion.length) { - this._appendSuggestions(query, suggestions); - } else if (suggestions.length) { - this._renderSuggestions(query, suggestions); - } else if (!this.$lastSuggestion.length && this.templates.notFound) { - this._renderNotFound(query); - } - this.trigger("rendered", this.name, suggestions, true); - }, - _renderSuggestions: function renderSuggestions(query, suggestions) { - var $fragment; - $fragment = this._getSuggestionsFragment(query, suggestions); - this.$lastSuggestion = $fragment.children().last(); - this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions)); - }, - _appendSuggestions: function appendSuggestions(query, suggestions) { - var $fragment, $lastSuggestion; - $fragment = this._getSuggestionsFragment(query, suggestions); - $lastSuggestion = $fragment.children().last(); - this.$lastSuggestion.after($fragment); - this.$lastSuggestion = $lastSuggestion; - }, - _renderPending: function renderPending(query) { - var template = this.templates.pending; - this._resetLastSuggestion(); - template && this.$el.html(template({ - query: query, - dataset: this.name - })); - }, - _renderNotFound: function renderNotFound(query) { - var template = this.templates.notFound; - this._resetLastSuggestion(); - template && this.$el.html(template({ - query: query, - dataset: this.name - })); - }, - _empty: function empty() { - this.$el.empty(); - this._resetLastSuggestion(); - }, - _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) { - var that = this, fragment; - fragment = document.createDocumentFragment(); - _.each(suggestions, function getSuggestionNode(suggestion) { - var $el, context; - context = that._injectQuery(query, suggestion); - $el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable); - fragment.appendChild($el[0]); - }); - this.highlight && highlight({ - className: this.classes.highlight, - node: fragment, - pattern: query - }); - return $(fragment); - }, - _getFooter: function getFooter(query, suggestions) { - return this.templates.footer ? this.templates.footer({ - query: query, - suggestions: suggestions, - dataset: this.name - }) : null; - }, - _getHeader: function getHeader(query, suggestions) { - return this.templates.header ? this.templates.header({ - query: query, - suggestions: suggestions, - dataset: this.name - }) : null; - }, - _resetLastSuggestion: function resetLastSuggestion() { - this.$lastSuggestion = $(); - }, - _injectQuery: function injectQuery(query, obj) { - return _.isObject(obj) ? _.mixin({ - _query: query - }, obj) : obj; - }, - update: function update(query) { - var that = this, canceled = false, syncCalled = false, rendered = 0; - this.cancel(); - this.cancel = function cancel() { - canceled = true; - that.cancel = $.noop; - that.async && that.trigger("asyncCanceled", query); - }; - this.source(query, sync, async); - !syncCalled && sync([]); - function sync(suggestions) { - if (syncCalled) { - return; - } - syncCalled = true; - suggestions = (suggestions || []).slice(0, that.limit); - rendered = suggestions.length; - that._overwrite(query, suggestions); - if (rendered < that.limit && that.async) { - that.trigger("asyncRequested", query); - } - } - function async(suggestions) { - suggestions = suggestions || []; - if (!canceled && rendered < that.limit) { - that.cancel = $.noop; - var idx = Math.abs(rendered - that.limit); - rendered += idx; - that._append(query, suggestions.slice(0, idx)); - that.async && that.trigger("asyncReceived", query); - } - } - }, - cancel: $.noop, - clear: function clear() { - this._empty(); - this.cancel(); - this.trigger("cleared"); - }, - isEmpty: function isEmpty() { - return this.$el.is(":empty"); - }, - destroy: function destroy() { - this.$el = $("
"); - } - }); - return Dataset; - function getDisplayFn(display) { - display = display || _.stringify; - return _.isFunction(display) ? display : displayFn; - function displayFn(obj) { - return obj[display]; - } - } - function getTemplates(templates, displayFn) { - return { - notFound: templates.notFound && _.templatify(templates.notFound), - pending: templates.pending && _.templatify(templates.pending), - header: templates.header && _.templatify(templates.header), - footer: templates.footer && _.templatify(templates.footer), - suggestion: templates.suggestion || suggestionTemplate - }; - function suggestionTemplate(context) { - return $("
").text(displayFn(context)); - } - } - function isValidName(str) { - return /^[_a-zA-Z0-9-]+$/.test(str); - } - }(); - var Menu = function() { - "use strict"; - function Menu(o, www) { - var that = this; - o = o || {}; - if (!o.node) { - $.error("node is required"); - } - www.mixin(this); - this.$node = $(o.node); - this.query = null; - this.datasets = _.map(o.datasets, initializeDataset); - function initializeDataset(oDataset) { - var node = that.$node.find(oDataset.node).first(); - oDataset.node = node.length ? node : $("
").appendTo(that.$node); - return new Dataset(oDataset, www); - } - } - _.mixin(Menu.prototype, EventEmitter, { - _onSelectableClick: function onSelectableClick($e) { - this.trigger("selectableClicked", $($e.currentTarget)); - }, - _onRendered: function onRendered(type, dataset, suggestions, async) { - this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); - this.trigger("datasetRendered", dataset, suggestions, async); - }, - _onCleared: function onCleared() { - this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); - this.trigger("datasetCleared"); - }, - _propagate: function propagate() { - this.trigger.apply(this, arguments); - }, - _allDatasetsEmpty: function allDatasetsEmpty() { - return _.every(this.datasets, isDatasetEmpty); - function isDatasetEmpty(dataset) { - return dataset.isEmpty(); - } - }, - _getSelectables: function getSelectables() { - return this.$node.find(this.selectors.selectable); - }, - _removeCursor: function _removeCursor() { - var $selectable = this.getActiveSelectable(); - $selectable && $selectable.removeClass(this.classes.cursor); - }, - _ensureVisible: function ensureVisible($el) { - var elTop, elBottom, nodeScrollTop, nodeHeight; - elTop = $el.position().top; - elBottom = elTop + $el.outerHeight(true); - nodeScrollTop = this.$node.scrollTop(); - nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10); - if (elTop < 0) { - this.$node.scrollTop(nodeScrollTop + elTop); - } else if (nodeHeight < elBottom) { - this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight)); - } - }, - bind: function() { - var that = this, onSelectableClick; - onSelectableClick = _.bind(this._onSelectableClick, this); - this.$node.on("click.tt", this.selectors.selectable, onSelectableClick); - this.$node.on("mouseover", this.selectors.selectable, function() { - that.setCursor($(this)); - }); - this.$node.on("mouseleave", function() { - that._removeCursor(); - }); - _.each(this.datasets, function(dataset) { - dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that); - }); - return this; - }, - isOpen: function isOpen() { - return this.$node.hasClass(this.classes.open); - }, - open: function open() { - this.$node.scrollTop(0); - this.$node.addClass(this.classes.open); - }, - close: function close() { - this.$node.removeClass(this.classes.open); - this._removeCursor(); - }, - setLanguageDirection: function setLanguageDirection(dir) { - this.$node.attr("dir", dir); - }, - selectableRelativeToCursor: function selectableRelativeToCursor(delta) { - var $selectables, $oldCursor, oldIndex, newIndex; - $oldCursor = this.getActiveSelectable(); - $selectables = this._getSelectables(); - oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1; - newIndex = oldIndex + delta; - newIndex = (newIndex + 1) % ($selectables.length + 1) - 1; - newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex; - return newIndex === -1 ? null : $selectables.eq(newIndex); - }, - setCursor: function setCursor($selectable) { - this._removeCursor(); - if ($selectable = $selectable && $selectable.first()) { - $selectable.addClass(this.classes.cursor); - this._ensureVisible($selectable); - } - }, - getSelectableData: function getSelectableData($el) { - return $el && $el.length ? Dataset.extractData($el) : null; - }, - getActiveSelectable: function getActiveSelectable() { - var $selectable = this._getSelectables().filter(this.selectors.cursor).first(); - return $selectable.length ? $selectable : null; - }, - getTopSelectable: function getTopSelectable() { - var $selectable = this._getSelectables().first(); - return $selectable.length ? $selectable : null; - }, - update: function update(query) { - var isValidUpdate = query !== this.query; - if (isValidUpdate) { - this.query = query; - _.each(this.datasets, updateDataset); - } - return isValidUpdate; - function updateDataset(dataset) { - dataset.update(query); - } - }, - empty: function empty() { - _.each(this.datasets, clearDataset); - this.query = null; - this.$node.addClass(this.classes.empty); - function clearDataset(dataset) { - dataset.clear(); - } - }, - destroy: function destroy() { - this.$node.off(".tt"); - this.$node = $("
"); - _.each(this.datasets, destroyDataset); - function destroyDataset(dataset) { - dataset.destroy(); - } - } - }); - return Menu; - }(); - var DefaultMenu = function() { - "use strict"; - var s = Menu.prototype; - function DefaultMenu() { - Menu.apply(this, [].slice.call(arguments, 0)); - } - _.mixin(DefaultMenu.prototype, Menu.prototype, { - open: function open() { - !this._allDatasetsEmpty() && this._show(); - return s.open.apply(this, [].slice.call(arguments, 0)); - }, - close: function close() { - this._hide(); - return s.close.apply(this, [].slice.call(arguments, 0)); - }, - _onRendered: function onRendered() { - if (this._allDatasetsEmpty()) { - this._hide(); - } else { - this.isOpen() && this._show(); - } - return s._onRendered.apply(this, [].slice.call(arguments, 0)); - }, - _onCleared: function onCleared() { - if (this._allDatasetsEmpty()) { - this._hide(); - } else { - this.isOpen() && this._show(); - } - return s._onCleared.apply(this, [].slice.call(arguments, 0)); - }, - setLanguageDirection: function setLanguageDirection(dir) { - this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl); - return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0)); - }, - _hide: function hide() { - this.$node.hide(); - }, - _show: function show() { - this.$node.css("display", "block"); - } - }); - return DefaultMenu; - }(); - var Typeahead = function() { - "use strict"; - function Typeahead(o, www) { - var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged; - o = o || {}; - if (!o.input) { - $.error("missing input"); - } - if (!o.menu) { - $.error("missing menu"); - } - if (!o.eventBus) { - $.error("missing event bus"); - } - www.mixin(this); - this.eventBus = o.eventBus; - this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; - this.input = o.input; - this.menu = o.menu; - this.enabled = true; - this.active = false; - this.input.hasFocus() && this.activate(); - this.dir = this.input.getLangDir(); - this._hacks(); - this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this); - onFocused = c(this, "activate", "open", "_onFocused"); - onBlurred = c(this, "deactivate", "_onBlurred"); - onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed"); - onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed"); - onEscKeyed = c(this, "isActive", "_onEscKeyed"); - onUpKeyed = c(this, "isActive", "open", "_onUpKeyed"); - onDownKeyed = c(this, "isActive", "open", "_onDownKeyed"); - onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed"); - onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed"); - onQueryChanged = c(this, "_openIfActive", "_onQueryChanged"); - onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged"); - this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this); - } - _.mixin(Typeahead.prototype, { - _hacks: function hacks() { - var $input, $menu; - $input = this.input.$input || $("
"); - $menu = this.menu.$node || $("
"); - $input.on("blur.tt", function($e) { - var active, isActive, hasActive; - active = document.activeElement; - isActive = $menu.is(active); - hasActive = $menu.has(active).length > 0; - if (_.isMsie() && (isActive || hasActive)) { - $e.preventDefault(); - $e.stopImmediatePropagation(); - _.defer(function() { - $input.focus(); - }); - } - }); - $menu.on("mousedown.tt", function($e) { - $e.preventDefault(); - }); - }, - _onSelectableClicked: function onSelectableClicked(type, $el) { - this.select($el); - }, - _onDatasetCleared: function onDatasetCleared() { - this._updateHint(); - }, - _onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) { - this._updateHint(); - this.eventBus.trigger("render", suggestions, async, dataset); - }, - _onAsyncRequested: function onAsyncRequested(type, dataset, query) { - this.eventBus.trigger("asyncrequest", query, dataset); - }, - _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) { - this.eventBus.trigger("asynccancel", query, dataset); - }, - _onAsyncReceived: function onAsyncReceived(type, dataset, query) { - this.eventBus.trigger("asyncreceive", query, dataset); - }, - _onFocused: function onFocused() { - this._minLengthMet() && this.menu.update(this.input.getQuery()); - }, - _onBlurred: function onBlurred() { - if (this.input.hasQueryChangedSinceLastFocus()) { - this.eventBus.trigger("change", this.input.getQuery()); - } - }, - _onEnterKeyed: function onEnterKeyed(type, $e) { - var $selectable; - if ($selectable = this.menu.getActiveSelectable()) { - this.select($selectable) && $e.preventDefault(); - } - }, - _onTabKeyed: function onTabKeyed(type, $e) { - var $selectable; - if ($selectable = this.menu.getActiveSelectable()) { - this.select($selectable) && $e.preventDefault(); - } else if ($selectable = this.menu.getTopSelectable()) { - this.autocomplete($selectable) && $e.preventDefault(); - } - }, - _onEscKeyed: function onEscKeyed() { - this.close(); - }, - _onUpKeyed: function onUpKeyed() { - this.moveCursor(-1); - }, - _onDownKeyed: function onDownKeyed() { - this.moveCursor(+1); - }, - _onLeftKeyed: function onLeftKeyed() { - if (this.dir === "rtl" && this.input.isCursorAtEnd()) { - this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable()); - } - }, - _onRightKeyed: function onRightKeyed() { - if (this.dir === "ltr" && this.input.isCursorAtEnd()) { - this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable()); - } - }, - _onQueryChanged: function onQueryChanged(e, query) { - this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty(); - }, - _onWhitespaceChanged: function onWhitespaceChanged() { - this._updateHint(); - }, - _onLangDirChanged: function onLangDirChanged(e, dir) { - if (this.dir !== dir) { - this.dir = dir; - this.menu.setLanguageDirection(dir); - } - }, - _openIfActive: function openIfActive() { - this.isActive() && this.open(); - }, - _minLengthMet: function minLengthMet(query) { - query = _.isString(query) ? query : this.input.getQuery() || ""; - return query.length >= this.minLength; - }, - _updateHint: function updateHint() { - var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match; - $selectable = this.menu.getTopSelectable(); - data = this.menu.getSelectableData($selectable); - val = this.input.getInputValue(); - if (data && !_.isBlankString(val) && !this.input.hasOverflow()) { - query = Input.normalizeQuery(val); - escapedQuery = _.escapeRegExChars(query); - frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); - match = frontMatchRegEx.exec(data.val); - match && this.input.setHint(val + match[1]); - } else { - this.input.clearHint(); - } - }, - isEnabled: function isEnabled() { - return this.enabled; - }, - enable: function enable() { - this.enabled = true; - }, - disable: function disable() { - this.enabled = false; - }, - isActive: function isActive() { - return this.active; - }, - activate: function activate() { - if (this.isActive()) { - return true; - } else if (!this.isEnabled() || this.eventBus.before("active")) { - return false; - } else { - this.active = true; - this.eventBus.trigger("active"); - return true; - } - }, - deactivate: function deactivate() { - if (!this.isActive()) { - return true; - } else if (this.eventBus.before("idle")) { - return false; - } else { - this.active = false; - this.close(); - this.eventBus.trigger("idle"); - return true; - } - }, - isOpen: function isOpen() { - return this.menu.isOpen(); - }, - open: function open() { - if (!this.isOpen() && !this.eventBus.before("open")) { - this.menu.open(); - this._updateHint(); - this.eventBus.trigger("open"); - } - return this.isOpen(); - }, - close: function close() { - if (this.isOpen() && !this.eventBus.before("close")) { - this.menu.close(); - this.input.clearHint(); - this.input.resetInputValue(); - this.eventBus.trigger("close"); - } - return !this.isOpen(); - }, - setVal: function setVal(val) { - this.input.setQuery(_.toStr(val)); - }, - getVal: function getVal() { - return this.input.getQuery(); - }, - select: function select($selectable) { - var data = this.menu.getSelectableData($selectable); - if (data && !this.eventBus.before("select", data.obj)) { - this.input.setQuery(data.val, true); - this.eventBus.trigger("select", data.obj); - this.close(); - return true; - } - return false; - }, - autocomplete: function autocomplete($selectable) { - var query, data, isValid; - query = this.input.getQuery(); - data = this.menu.getSelectableData($selectable); - isValid = data && query !== data.val; - if (isValid && !this.eventBus.before("autocomplete", data.obj)) { - this.input.setQuery(data.val); - this.eventBus.trigger("autocomplete", data.obj); - return true; - } - return false; - }, - moveCursor: function moveCursor(delta) { - var query, $candidate, data, payload, cancelMove; - query = this.input.getQuery(); - $candidate = this.menu.selectableRelativeToCursor(delta); - data = this.menu.getSelectableData($candidate); - payload = data ? data.obj : null; - cancelMove = this._minLengthMet() && this.menu.update(query); - if (!cancelMove && !this.eventBus.before("cursorchange", payload)) { - this.menu.setCursor($candidate); - if (data) { - this.input.setInputValue(data.val); - } else { - this.input.resetInputValue(); - this._updateHint(); - } - this.eventBus.trigger("cursorchange", payload); - return true; - } - return false; - }, - destroy: function destroy() { - this.input.destroy(); - this.menu.destroy(); - } - }); - return Typeahead; - function c(ctx) { - var methods = [].slice.call(arguments, 1); - return function() { - var args = [].slice.call(arguments); - _.each(methods, function(method) { - return ctx[method].apply(ctx, args); - }); - }; - } - }(); - (function() { - "use strict"; - var old, keys, methods; - old = $.fn.typeahead; - keys = { - www: "tt-www", - attrs: "tt-attrs", - typeahead: "tt-typeahead" - }; - methods = { - initialize: function initialize(o, datasets) { - var www; - datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); - o = o || {}; - www = WWW(o.classNames); - return this.each(attach); - function attach() { - var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor; - _.each(datasets, function(d) { - d.highlight = !!o.highlight; - }); - $input = $(this); - $wrapper = $(www.html.wrapper); - $hint = $elOrNull(o.hint); - $menu = $elOrNull(o.menu); - defaultHint = o.hint !== false && !$hint; - defaultMenu = o.menu !== false && !$menu; - defaultHint && ($hint = buildHintFromInput($input, www)); - defaultMenu && ($menu = $(www.html.menu).css(www.css.menu)); - $hint && $hint.val(""); - $input = prepInput($input, www); - if (defaultHint || defaultMenu) { - $wrapper.css(www.css.wrapper); - $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint); - $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null); - } - MenuConstructor = defaultMenu ? DefaultMenu : Menu; - eventBus = new EventBus({ - el: $input - }); - input = new Input({ - hint: $hint, - input: $input - }, www); - menu = new MenuConstructor({ - node: $menu, - datasets: datasets - }, www); - typeahead = new Typeahead({ - input: input, - menu: menu, - eventBus: eventBus, - minLength: o.minLength - }, www); - $input.data(keys.www, www); - $input.data(keys.typeahead, typeahead); - } - }, - isEnabled: function isEnabled() { - var enabled; - ttEach(this.first(), function(t) { - enabled = t.isEnabled(); - }); - return enabled; - }, - enable: function enable() { - ttEach(this, function(t) { - t.enable(); - }); - return this; - }, - disable: function disable() { - ttEach(this, function(t) { - t.disable(); - }); - return this; - }, - isActive: function isActive() { - var active; - ttEach(this.first(), function(t) { - active = t.isActive(); - }); - return active; - }, - activate: function activate() { - ttEach(this, function(t) { - t.activate(); - }); - return this; - }, - deactivate: function deactivate() { - ttEach(this, function(t) { - t.deactivate(); - }); - return this; - }, - isOpen: function isOpen() { - var open; - ttEach(this.first(), function(t) { - open = t.isOpen(); - }); - return open; - }, - open: function open() { - ttEach(this, function(t) { - t.open(); - }); - return this; - }, - close: function close() { - ttEach(this, function(t) { - t.close(); - }); - return this; - }, - select: function select(el) { - var success = false, $el = $(el); - ttEach(this.first(), function(t) { - success = t.select($el); - }); - return success; - }, - autocomplete: function autocomplete(el) { - var success = false, $el = $(el); - ttEach(this.first(), function(t) { - success = t.autocomplete($el); - }); - return success; - }, - moveCursor: function moveCursoe(delta) { - var success = false; - ttEach(this.first(), function(t) { - success = t.moveCursor(delta); - }); - return success; - }, - val: function val(newVal) { - var query; - if (!arguments.length) { - ttEach(this.first(), function(t) { - query = t.getVal(); - }); - return query; - } else { - ttEach(this, function(t) { - t.setVal(_.toStr(newVal)); - }); - return this; - } - }, - destroy: function destroy() { - ttEach(this, function(typeahead, $input) { - revert($input); - typeahead.destroy(); - }); - return this; - } - }; - $.fn.typeahead = function(method) { - if (methods[method]) { - return methods[method].apply(this, [].slice.call(arguments, 1)); - } else { - return methods.initialize.apply(this, arguments); - } - }; - $.fn.typeahead.noConflict = function noConflict() { - $.fn.typeahead = old; - return this; - }; - function ttEach($els, fn) { - $els.each(function() { - var $input = $(this), typeahead; - (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input); - }); - } - function buildHintFromInput($input, www) { - return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({ - autocomplete: "off", - spellcheck: "false", - tabindex: -1 - }); - } - function prepInput($input, www) { - $input.data(keys.attrs, { - dir: $input.attr("dir"), - autocomplete: $input.attr("autocomplete"), - spellcheck: $input.attr("spellcheck"), - style: $input.attr("style") - }); - $input.addClass(www.classes.input).attr({ - autocomplete: "off", - spellcheck: false - }); - try { - !$input.attr("dir") && $input.attr("dir", "auto"); - } catch (e) {} - return $input; - } - function getBackgroundStyles($el) { - return { - backgroundAttachment: $el.css("background-attachment"), - backgroundClip: $el.css("background-clip"), - backgroundColor: $el.css("background-color"), - backgroundImage: $el.css("background-image"), - backgroundOrigin: $el.css("background-origin"), - backgroundPosition: $el.css("background-position"), - backgroundRepeat: $el.css("background-repeat"), - backgroundSize: $el.css("background-size") - }; - } - function revert($input) { - var www, $wrapper; - www = $input.data(keys.www); - $wrapper = $input.parent().filter(www.selectors.wrapper); - _.each($input.data(keys.attrs), function(val, key) { - _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); - }); - $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input); - if ($wrapper.length) { - $input.detach().insertAfter($wrapper); - $wrapper.remove(); - } - } - function $elOrNull(obj) { - var isValid, $el; - isValid = _.isJQuery(obj) || _.isElement(obj); - $el = isValid ? $(obj).first() : []; - return $el.length ? $el : null; - } - })(); -}); \ No newline at end of file From 9bca03d5e970bc3a161c0633b4845bd3847f5219 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Fri, 18 Nov 2016 00:57:32 +0100 Subject: [PATCH 36/71] Refactor ShareVisibilitesController closes #7196 --- Changelog.md | 1 + .../share_visibilities_controller.rb | 13 +++---- .../share_visibilities_controller_spec.rb | 34 ++++++++++++------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/Changelog.md b/Changelog.md index 42506b441..1bbd260aa 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ * Force jasmine fails on syntax errors [#7185](https://github.com/diaspora/diaspora/pull/7185) * Don't display mail-related view content if it is disabled in the pod's config [#7190](https://github.com/diaspora/diaspora/pull/7190) * Use typeahead.js from rails-assets.org [#7192](https://github.com/diaspora/diaspora/pull/7192) +* Refactor ShareVisibilitesController to use PostService [#7196](https://github.com/diaspora/diaspora/pull/7196) ## Bug fixes * Fix fetching comments after fetching likes [#7167](https://github.com/diaspora/diaspora/pull/7167) diff --git a/app/controllers/share_visibilities_controller.rb b/app/controllers/share_visibilities_controller.rb index bbc7bba16..bf1ea1f10 100644 --- a/app/controllers/share_visibilities_controller.rb +++ b/app/controllers/share_visibilities_controller.rb @@ -7,17 +7,14 @@ class ShareVisibilitiesController < ApplicationController before_action :authenticate_user! def update - #note :id references a postvisibility - params[:shareable_id] ||= params[:post_id] - params[:shareable_type] ||= 'Post' - - vis = current_user.toggle_hidden_shareable(accessible_post) - render :nothing => true, :status => 200 + post = post_service.find!(params[:post_id]) + current_user.toggle_hidden_shareable(post) + head :ok end private - def accessible_post - @post ||= params[:shareable_type].constantize.where(:id => params[:post_id]).select("id, guid, author_id, created_at").first + def post_service + @post_service ||= PostService.new(current_user) end end diff --git a/spec/controllers/share_visibilities_controller_spec.rb b/spec/controllers/share_visibilities_controller_spec.rb index 6215de699..9b61cc80c 100644 --- a/spec/controllers/share_visibilities_controller_spec.rb +++ b/spec/controllers/share_visibilities_controller_spec.rb @@ -7,32 +7,42 @@ require 'spec_helper' describe ShareVisibilitiesController, :type => :controller do before do @status = alice.post(:status_message, :text => "hello", :to => alice.aspects.first) - sign_in(bob, scope: :user) end describe '#update' do context "on a post you can see" do + before do + sign_in(bob, scope: :user) + end + it 'succeeds' do put :update, :format => :js, :id => 42, :post_id => @status.id expect(response).to be_success end it 'it calls toggle_hidden_shareable' do - expect(@controller.current_user).to receive(:toggle_hidden_shareable).with(an_instance_of(Post)) + expect(@controller.current_user).to receive(:toggle_hidden_shareable).with(an_instance_of(StatusMessage)) put :update, :format => :js, :id => 42, :post_id => @status.id end end - end - - describe "#accessible_post" do - it "memoizes a query for a post given a post_id param" do - id = 1 - @controller.params[:post_id] = id - @controller.params[:shareable_type] = 'Post' - expect(Post).to receive(:where).with(hash_including(:id => id)).once.and_return(double.as_null_object) - 2.times do |n| - @controller.send(:accessible_post) + context "on a post you can't see" do + before do + sign_in(eve, scope: :user) + end + + it "raises an error" do + expect { + put :update, format: :js, id: 42, post_id: @status.id + }.to raise_error ActiveRecord::RecordNotFound + end + + it "it doesn't call toggle_hidden_shareable" do + expect(@controller.current_user).not_to receive(:toggle_hidden_shareable).with(an_instance_of(StatusMessage)) + begin + put :update, format: :js, id: 42, post_id: @status.id + rescue ActiveRecord::RecordNotFound + end end end end From 9b72527f3e4991dbfd6f7103e343eaf9acf0beba Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Fri, 18 Nov 2016 01:36:32 +0100 Subject: [PATCH 37/71] Refactor conversations controller creation specs closes #7197 --- .../conversations_controller_spec.rb | 64 +++++-------------- 1 file changed, 16 insertions(+), 48 deletions(-) diff --git a/spec/controllers/conversations_controller_spec.rb b/spec/controllers/conversations_controller_spec.rb index d02ed22fe..91226b902 100644 --- a/spec/controllers/conversations_controller_spec.rb +++ b/spec/controllers/conversations_controller_spec.rb @@ -199,15 +199,11 @@ describe ConversationsController, :type => :controller do end it "does not create a conversation" do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) + expect { post :create, @hash }.not_to change(Conversation, :count) end it "does not create a message" do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) + expect { post :create, @hash }.not_to change(Message, :count) end it "responds with an error message" do @@ -227,15 +223,11 @@ describe ConversationsController, :type => :controller do end it "does not create a conversation" do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) + expect { post :create, @hash }.not_to change(Conversation, :count) end it "does not create a message" do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) + expect { post :create, @hash }.not_to change(Message, :count) end it "responds with an error message" do @@ -255,15 +247,11 @@ describe ConversationsController, :type => :controller do end it "does not create a conversation" do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) + expect { post :create, @hash }.not_to change(Conversation, :count) end it "does not create a message" do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) + expect { post :create, @hash }.not_to change(Message, :count) end it "responds with an error message" do @@ -288,15 +276,11 @@ describe ConversationsController, :type => :controller do end it "does not create a conversation" do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) + expect { post :create, @hash }.not_to change(Conversation, :count) end it "does not create a message" do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) + expect { post :create, @hash }.not_to change(Message, :count) end it "responds with an error message" do @@ -385,15 +369,11 @@ describe ConversationsController, :type => :controller do end it "does not create a conversation" do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) + expect { post :create, @hash }.not_to change(Conversation, :count) end it "does not create a message" do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) + expect { post :create, @hash }.not_to change(Message, :count) end it "responds with an error message" do @@ -413,15 +393,11 @@ describe ConversationsController, :type => :controller do end it "does not create a conversation" do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) + expect { post :create, @hash }.not_to change(Conversation, :count) end it "does not create a message" do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) + expect { post :create, @hash }.not_to change(Message, :count) end it "responds with an error message" do @@ -441,15 +417,11 @@ describe ConversationsController, :type => :controller do end it "does not create a conversation" do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) + expect { post :create, @hash }.not_to change(Conversation, :count) end it "does not create a message" do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) + expect { post :create, @hash }.not_to change(Message, :count) end it "responds with an error message" do @@ -471,15 +443,11 @@ describe ConversationsController, :type => :controller do end it "does not create a conversation" do - count = Conversation.count - post :create, @hash - expect(Conversation.count).to eq(count) + expect { post :create, @hash }.not_to change(Conversation, :count) end it "does not create a message" do - count = Message.count - post :create, @hash - expect(Message.count).to eq(count) + expect { post :create, @hash }.not_to change(Message, :count) end it "responds with an error message" do From af331bfb30bc094432fe2d8b4cab4aa00b9fd23a Mon Sep 17 00:00:00 2001 From: Augier Date: Mon, 8 Aug 2016 16:56:02 +0200 Subject: [PATCH 38/71] Add collection to app.views.NotificationDropdown and app.views.Notifications closes #6952 --- Changelog.md | 2 + app/assets/javascripts/app/app.js | 1 + .../app/collections/notifications.js | 118 +++++++++ .../javascripts/app/models/notification.js | 69 +++++ app/assets/javascripts/app/router.js | 2 +- .../javascripts/app/views/header_view.js | 10 +- .../app/views/notification_dropdown_view.js | 71 ++--- .../app/views/notifications_view.js | 139 +++++----- .../helpers/browser_notification.js | 22 ++ app/assets/templates/header_tpl.jst.hbs | 9 +- app/controllers/notifications_controller.rb | 13 +- app/views/notifications/index.html.haml | 3 +- config/locales/javascript/javascript.en.yml | 3 + .../jasmine_fixtures/notifications_spec.rb | 2 + .../notifications_controller_spec.rb | 17 +- .../notifications_collection_spec.js | 249 ++++++++++++++++++ .../app/models/notification_spec.js | 85 ++++++ .../javascripts/app/views/header_view_spec.js | 1 + .../views/notification_dropdown_view_spec.js | 143 ++++------ .../app/views/notifications_view_spec.js | 220 +++++++++++----- 20 files changed, 875 insertions(+), 304 deletions(-) create mode 100644 app/assets/javascripts/app/collections/notifications.js create mode 100644 app/assets/javascripts/app/models/notification.js create mode 100644 app/assets/javascripts/helpers/browser_notification.js create mode 100644 spec/javascripts/app/collections/notifications_collection_spec.js create mode 100644 spec/javascripts/app/models/notification_spec.js diff --git a/Changelog.md b/Changelog.md index 1bbd260aa..ac93a6c50 100644 --- a/Changelog.md +++ b/Changelog.md @@ -19,6 +19,8 @@ * Add a dark color theme [#7152](https://github.com/diaspora/diaspora/pull/7152) * Added setting for custom changelog URL [#7166](https://github.com/diaspora/diaspora/pull/7166) * Show more information of recipients on conversation creation [#7129](https://github.com/diaspora/diaspora/pull/7129) +* Update notifications every 5 minutes and when opening the notification dropdown [#6952](https://github.com/diaspora/diaspora/pull/6952) +* Show browser notifications when receiving new unread notifications [#6952](https://github.com/diaspora/diaspora/pull/6952) # 0.6.1.0 diff --git a/app/assets/javascripts/app/app.js b/app/assets/javascripts/app/app.js index 1046bd66a..d300be7e6 100644 --- a/app/assets/javascripts/app/app.js +++ b/app/assets/javascripts/app/app.js @@ -90,6 +90,7 @@ var app = { setupHeader: function() { if(app.currentUser.authenticated()) { + app.notificationsCollection = new app.collections.Notifications(); app.header = new app.views.Header(); $("header").prepend(app.header.el); app.header.render(); diff --git a/app/assets/javascripts/app/collections/notifications.js b/app/assets/javascripts/app/collections/notifications.js new file mode 100644 index 000000000..0bb4a2f97 --- /dev/null +++ b/app/assets/javascripts/app/collections/notifications.js @@ -0,0 +1,118 @@ +app.collections.Notifications = Backbone.Collection.extend({ + model: app.models.Notification, + // URL parameter + /* eslint-disable camelcase */ + url: Routes.notifications({per_page: 10, page: 1}), + /* eslint-enable camelcase */ + page: 2, + perPage: 5, + unreadCount: 0, + unreadCountByType: {}, + timeout: 300000, // 5 minutes + + initialize: function() { + this.pollNotifications(); + + setTimeout(function() { + setInterval(this.pollNotifications.bind(this), this.timeout); + }.bind(this), this.timeout); + + Diaspora.BrowserNotification.requestPermission(); + }, + + pollNotifications: function() { + var unreadCountBefore = this.unreadCount; + this.fetch(); + + this.once("finishedLoading", function() { + if (unreadCountBefore < this.unreadCount) { + Diaspora.BrowserNotification.spawnNotification( + Diaspora.I18n.t("notifications.new_notifications", {count: this.unreadCount})); + } + }, this); + }, + + fetch: function(options) { + options = options || {}; + options.remove = false; + options.merge = true; + options.parse = true; + Backbone.Collection.prototype.fetch.apply(this, [options]); + }, + + fetchMore: function() { + var hasMoreNotifications = (this.page * this.perPage) <= this.length; + // There are more notifications to load on the current page + if (hasMoreNotifications) { + this.page++; + // URL parameter + /* eslint-disable camelcase */ + var route = Routes.notifications({per_page: this.perPage, page: this.page}); + /* eslint-enable camelcase */ + this.fetch({url: route, pushBack: true}); + } + }, + + /** + * Adds new models to the collection at the end or at the beginning of the collection and + * then fires an event for each model of the collection. It will fire a different event + * based on whether the models were added at the end (typically when the scroll triggers to load more + * notifications) or at the beginning (new notifications have been added to the front of the list). + */ + set: function(items, options) { + options = options || {}; + options.at = options.pushBack ? this.length : 0; + + // Retreive back the new created models + var models = []; + var accu = function(model) { models.push(model); }; + this.on("add", accu); + Backbone.Collection.prototype.set.apply(this, [items, options]); + this.off("add", accu); + + if (options.pushBack) { + models.forEach(function(model) { this.trigger("pushBack", model); }.bind(this)); + } else { + // Fires events in the reverse order so that the first event is prepended in first position + models.reverse(); + models.forEach(function(model) { this.trigger("pushFront", model); }.bind(this)); + } + this.trigger("finishedLoading"); + }, + + parse: function(response) { + this.unreadCount = response.unread_count; + this.unreadCountByType = response.unread_count_by_type; + + return _.map(response.notification_list, function(item) { + /* eslint-disable new-cap */ + var model = new this.model(item); + /* eslint-enable new-cap */ + model.on("change:unread", this.onChangedUnreadStatus.bind(this)); + return model; + }.bind(this)); + }, + + setAllRead: function() { + this.forEach(function(model) { model.setRead(); }); + }, + + setRead: function(guid) { + this.find(function(model) { return model.guid === guid; }).setRead(); + }, + + setUnread: function(guid) { + this.find(function(model) { return model.guid === guid; }).setUnread(); + }, + + onChangedUnreadStatus: function(model) { + if (model.get("unread") === true) { + this.unreadCount++; + this.unreadCountByType[model.get("type")]++; + } else { + this.unreadCount = Math.max(this.unreadCount - 1, 0); + this.unreadCountByType[model.get("type")] = Math.max(this.unreadCountByType[model.get("type")] - 1, 0); + } + this.trigger("update"); + } +}); diff --git a/app/assets/javascripts/app/models/notification.js b/app/assets/javascripts/app/models/notification.js new file mode 100644 index 000000000..b2e342116 --- /dev/null +++ b/app/assets/javascripts/app/models/notification.js @@ -0,0 +1,69 @@ +app.models.Notification = Backbone.Model.extend({ + constructor: function(attributes, options) { + options = options || {}; + options.parse = true; + Backbone.Model.apply(this, [attributes, options]); + this.guid = this.get("id"); + }, + + /** + * Flattens the notification object returned by the server. + * + * The server returns an object that looks like: + * + * { + * "reshared": { + * "id": 45, + * "target_type": "Post", + * "target_id": 11, + * "recipient_id": 1, + * "unread": true, + * "created_at": "2015-10-27T19:56:30.000Z", + * "updated_at": "2015-10-27T19:56:30.000Z", + * "note_html": + * }, + * "type": "reshared" + * } + * + * The returned object looks like: + * + * { + * "type": "reshared", + * "id": 45, + * "target_type": "Post", + * "target_id": 11, + * "recipient_id": 1, + * "unread": true, + * "created_at": "2015-10-27T19:56:30.000Z", + * "updated_at": "2015-10-27T19:56:30.000Z", + * "note_html": , + * } + */ + parse: function(response) { + var result = {type: response.type}; + result = $.extend(result, response[result.type]); + return result; + }, + + setRead: function() { + this.setUnreadStatus(false); + }, + + setUnread: function() { + this.setUnreadStatus(true); + }, + + setUnreadStatus: function(state) { + if (this.get("unread") !== state) { + $.ajax({ + url: Routes.notification(this.guid), + /* eslint-disable camelcase */ + data: {set_unread: state}, + /* eslint-enable camelcase */ + type: "PUT", + context: this, + success: function() { this.set("unread", state); } + }); + } + } +}); diff --git a/app/assets/javascripts/app/router.js b/app/assets/javascripts/app/router.js index 1884bb880..ed8ec9d9a 100644 --- a/app/assets/javascripts/app/router.js +++ b/app/assets/javascripts/app/router.js @@ -139,7 +139,7 @@ app.Router = Backbone.Router.extend({ notifications: function() { this._loadContacts(); this.renderAspectMembershipDropdowns($(document)); - new app.views.Notifications({el: "#notifications_container"}); + new app.views.Notifications({el: "#notifications_container", collection: app.notificationsCollection}); }, peopleSearch: function() { diff --git a/app/assets/javascripts/app/views/header_view.js b/app/assets/javascripts/app/views/header_view.js index 5b682c3b3..496cd83d3 100644 --- a/app/assets/javascripts/app/views/header_view.js +++ b/app/assets/javascripts/app/views/header_view.js @@ -12,12 +12,12 @@ app.views.Header = app.views.Base.extend({ }); }, - postRenderTemplate: function(){ - new app.views.Notifications({ el: "#notification-dropdown" }); - this.notificationDropdown = new app.views.NotificationDropdown({ el: "#notification-dropdown" }); - new app.views.Search({ el: "#header-search-form" }); + postRenderTemplate: function() { + new app.views.Notifications({el: "#notification-dropdown", collection: app.notificationsCollection}); + new app.views.NotificationDropdown({el: "#notification-dropdown", collection: app.notificationsCollection}); + new app.views.Search({el: "#header-search-form"}); }, - menuElement: function(){ return this.$("ul.dropdown"); }, + menuElement: function() { return this.$("ul.dropdown"); } }); // @license-end diff --git a/app/assets/javascripts/app/views/notification_dropdown_view.js b/app/assets/javascripts/app/views/notification_dropdown_view.js index a44556c9a..a72f1e8f1 100644 --- a/app/assets/javascripts/app/views/notification_dropdown_view.js +++ b/app/assets/javascripts/app/views/notification_dropdown_view.js @@ -6,16 +6,21 @@ app.views.NotificationDropdown = app.views.Base.extend({ }, initialize: function(){ - $(document.body).click($.proxy(this.hideDropdown, this)); + $(document.body).click(this.hideDropdown.bind(this)); - this.notifications = []; - this.perPage = 5; - this.hasMoreNotifs = true; this.badge = this.$el; this.dropdown = $("#notification-dropdown"); this.dropdownNotifications = this.dropdown.find(".notifications"); this.ajaxLoader = this.dropdown.find(".ajax-loader"); this.perfectScrollbarInitialized = false; + this.dropdownNotifications.scroll(this.dropdownScroll.bind(this)); + this.bindCollectionEvents(); + }, + + bindCollectionEvents: function() { + this.collection.on("pushFront", this.onPushFront.bind(this)); + this.collection.on("pushBack", this.onPushBack.bind(this)); + this.collection.on("finishedLoading", this.finishLoading.bind(this)); }, toggleDropdown: function(evt){ @@ -31,12 +36,11 @@ app.views.NotificationDropdown = app.views.Base.extend({ }, showDropdown: function(){ - this.resetParams(); this.ajaxLoader.show(); this.dropdown.addClass("dropdown-open"); this.updateScrollbar(); this.dropdownNotifications.addClass("loading"); - this.getNotifications(); + this.collection.fetch(); }, hideDropdown: function(evt){ @@ -50,40 +54,18 @@ app.views.NotificationDropdown = app.views.Base.extend({ dropdownScroll: function(){ var isLoading = ($(".loading").length === 1); - if (this.isBottom() && this.hasMoreNotifs && !isLoading){ + if (this.isBottom() && !isLoading) { this.dropdownNotifications.addClass("loading"); - this.getNotifications(); + this.collection.fetchMore(); } }, - getParams: function(){ - if(this.notifications.length === 0){ return{ per_page: 10, page: 1 }; } - else{ return{ per_page: this.perPage, page: this.nextPage }; } - }, - - resetParams: function(){ - this.notifications.length = 0; - this.hasMoreNotifs = true; - delete this.nextPage; - }, - isBottom: function(){ var bottom = this.dropdownNotifications.prop("scrollHeight") - this.dropdownNotifications.height(); var currentPosition = this.dropdownNotifications.scrollTop(); return currentPosition + 50 >= bottom; }, - getNotifications: function(){ - var self = this; - $.getJSON(Routes.notifications(this.getParams()), function(notifications){ - $.each(notifications, function(){ self.notifications.push(this); }); - self.hasMoreNotifs = notifications.length >= self.perPage; - if(self.nextPage){ self.nextPage++; } - else { self.nextPage = 3; } - self.renderNotifications(); - }); - }, - hideAjaxLoader: function(){ var self = this; this.ajaxLoader.find(".spinner").fadeTo(200, 0, function(){ @@ -93,28 +75,23 @@ app.views.NotificationDropdown = app.views.Base.extend({ }); }, - renderNotifications: function(){ - var self = this; - this.dropdownNotifications.find(".media.stream-element").remove(); - $.each(self.notifications, function(index, notifications){ - $.each(notifications, function(index, notification){ - if($.inArray(notification, notifications) === -1){ - var node = self.dropdownNotifications.append(notification.note_html); - $(node).find(".unread-toggle .entypo-eye").tooltip("destroy").tooltip(); - $(node).find(self.avatars.selector).error(self.avatars.fallback); - } - }); - }); + onPushBack: function(notification) { + var node = this.dropdownNotifications.append(notification.get("note_html")); + $(node).find(".unread-toggle .entypo-eye").tooltip("destroy").tooltip(); + $(node).find(this.avatars.selector).error(this.avatars.fallback); + }, - this.hideAjaxLoader(); + onPushFront: function(notification) { + var node = this.dropdownNotifications.prepend(notification.get("note_html")); + $(node).find(".unread-toggle .entypo-eye").tooltip("destroy").tooltip(); + $(node).find(this.avatars.selector).error(this.avatars.fallback); + }, + finishLoading: function() { app.helpers.timeago(this.dropdownNotifications); - this.updateScrollbar(); + this.hideAjaxLoader(); this.dropdownNotifications.removeClass("loading"); - this.dropdownNotifications.scroll(function(){ - self.dropdownScroll(); - }); }, updateScrollbar: function() { diff --git a/app/assets/javascripts/app/views/notifications_view.js b/app/assets/javascripts/app/views/notifications_view.js index 08400742e..3ae156348 100644 --- a/app/assets/javascripts/app/views/notifications_view.js +++ b/app/assets/javascripts/app/views/notifications_view.js @@ -1,96 +1,85 @@ // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later app.views.Notifications = Backbone.View.extend({ - events: { - "click .unread-toggle" : "toggleUnread", - "click #mark_all_read_link": "markAllRead" + "click .unread-toggle": "toggleUnread", + "click #mark-all-read-link": "markAllRead" }, initialize: function() { $(".unread-toggle .entypo-eye").tooltip(); app.helpers.timeago($(document)); + this.bindCollectionEvents(); + }, + + bindCollectionEvents: function() { + this.collection.on("change", this.onChangedUnreadStatus.bind(this)); + this.collection.on("update", this.updateView.bind(this)); }, toggleUnread: function(evt) { var note = $(evt.target).closest(".stream-element"); var unread = note.hasClass("unread"); var guid = note.data("guid"); - if (unread){ this.setRead(guid); } - else { this.setUnread(guid); } - }, - - getAllUnread: function() { return $(".media.stream-element.unread"); }, - - setRead: function(guid) { this.setUnreadStatus(guid, false); }, - - setUnread: function(guid){ this.setUnreadStatus(guid, true); }, - - setUnreadStatus: function(guid, state){ - $.ajax({ - url: "/notifications/" + guid, - data: { set_unread: state }, - type: "PUT", - context: this, - success: this.clickSuccess - }); - }, - - clickSuccess: function(data) { - var guid = data.guid; - var type = $(".stream-element[data-guid=" + guid + "]").data("type"); - this.updateView(guid, type, data.unread); - }, - - markAllRead: function(evt){ - if(evt) { evt.preventDefault(); } - var self = this; - this.getAllUnread().each(function(i, el){ - self.setRead($(el).data("guid")); - }); - }, - - updateView: function(guid, type, unread) { - var change = unread ? 1 : -1, - allNotes = $("#notifications_container .list-group > a:eq(0) .badge"), - typeNotes = $("#notifications_container .list-group > a[data-type=" + type + "] .badge"), - headerBadge = $(".notifications-link .badge"), - note = $(".notifications .stream-element[data-guid=" + guid + "]"), - markAllReadLink = $("a#mark_all_read_link"), - translationKey = unread ? "notifications.mark_read" : "notifications.mark_unread"; - - if(unread){ note.removeClass("read").addClass("unread"); } - else { note.removeClass("unread").addClass("read"); } - - $(".unread-toggle .entypo-eye", note) - .tooltip("destroy") - .removeAttr("data-original-title") - .attr("title",Diaspora.I18n.t(translationKey)) - .tooltip(); - - [allNotes, typeNotes, headerBadge].forEach(function(element){ - element.text(function(i, text){ - return parseInt(text) + change; - }); - }); - - [allNotes, typeNotes].forEach(function(badge) { - if(badge.text() > 0) { - badge.removeClass("hidden"); - } - else { - badge.addClass("hidden"); - } - }); - - if(headerBadge.text() > 0){ - headerBadge.removeClass("hidden"); - markAllReadLink.removeClass("disabled"); + if (unread) { + this.collection.setRead(guid); + } else { + this.collection.setUnread(guid); } - else{ - headerBadge.addClass("hidden"); + }, + + markAllRead: function() { + this.collection.setAllRead(); + }, + + onChangedUnreadStatus: function(model) { + var unread = model.get("unread"); + var translationKey = unread ? "notifications.mark_read" : "notifications.mark_unread"; + var note = $(".stream-element[data-guid=" + model.guid + "]"); + + note.find(".entypo-eye") + .tooltip("destroy") + .removeAttr("data-original-title") + .attr("title", Diaspora.I18n.t(translationKey)) + .tooltip(); + + if (unread) { + note.removeClass("read").addClass("unread"); + } else { + note.removeClass("unread").addClass("read"); + } + }, + + updateView: function() { + var notificationsContainer = $("#notifications_container"); + + // update notification counts in the sidebar + Object.keys(this.collection.unreadCountByType).forEach(function(notificationType) { + var count = this.collection.unreadCountByType[notificationType]; + this.updateBadge(notificationsContainer.find("a[data-type=" + notificationType + "] .badge"), count); + }.bind(this)); + + this.updateBadge(notificationsContainer.find("a[data-type=all] .badge"), this.collection.unreadCount); + + // update notification count in the header + this.updateBadge($(".notifications-link .badge"), this.collection.unreadCount); + + var markAllReadLink = $("a#mark-all-read-link"); + + if (this.collection.unreadCount > 0) { + markAllReadLink.removeClass("disabled"); + } else { markAllReadLink.addClass("disabled"); } + }, + + updateBadge: function(badge, count) { + badge.text(count); + if (count > 0) { + badge.removeClass("hidden"); + } else { + badge.addClass("hidden"); + } } }); // @license-end diff --git a/app/assets/javascripts/helpers/browser_notification.js b/app/assets/javascripts/helpers/browser_notification.js new file mode 100644 index 000000000..e8c8b7d8f --- /dev/null +++ b/app/assets/javascripts/helpers/browser_notification.js @@ -0,0 +1,22 @@ +Diaspora.BrowserNotification = { + requestPermission: function() { + if ("Notification" in window && Notification.permission !== "granted" && Notification.permission !== "denied") { + Notification.requestPermission(); + } + }, + + spawnNotification: function(title, summary) { + if ("Notification" in window && Notification.permission === "granted") { + if (!_.isString(title)) { + throw new Error("No notification title given."); + } + + summary = summary || ""; + + new Notification(title, { + body: summary, + icon: ImagePaths.get("branding/logos/asterisk_white_mobile.png") + }); + } + } +}; diff --git a/app/assets/templates/header_tpl.jst.hbs b/app/assets/templates/header_tpl.jst.hbs index a895d99c9..ab8a27a95 100644 --- a/app/assets/templates/header_tpl.jst.hbs +++ b/app/assets/templates/header_tpl.jst.hbs @@ -53,7 +53,7 @@